diff --git a/src/WorldshaperLauncher.tsx b/src/WorldshaperLauncher.tsx index ea8e857..f7efd48 100644 --- a/src/WorldshaperLauncher.tsx +++ b/src/WorldshaperLauncher.tsx @@ -22,7 +22,6 @@ type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error"; type BoardTab = "news" | "requests"; type LauncherWindowMode = "public" | "admin"; type LauncherRequestStatus = "pending" | "active" | "implemented"; -type AdminDetailTab = "routing" | "analysis"; type LauncherRequestAnalysisRouting = { summary?: string; @@ -147,232 +146,11 @@ function readLauncherWindowMode(): LauncherWindowMode { return searchParams.get("admin") === "requests" ? "admin" : "public"; } -function normalizeStringList(values: string[]): string[] { - return Array.from(new Set( - values - .map((entry) => String(entry || "").trim()) - .filter(Boolean), - )).sort((left, right) => left.localeCompare(right)); -} - -function appendUniqueString(values: string[], value: string): string[] { - const normalizedValue = String(value || "").trim(); - if (!normalizedValue) { - return normalizeStringList(values); - } - return normalizeStringList([...values, normalizedValue]); -} - -function removeStringValue(values: string[], value: string): string[] { - const normalizedValue = String(value || "").trim().toLowerCase(); - return normalizeStringList(values.filter((entry) => entry.trim().toLowerCase() !== normalizedValue)); -} - -function toggleStringSelection(current: string[], value: string): string[] { - return current.includes(value) - ? current.filter((entry) => entry !== value) - : [...current, value].sort((left, right) => left.localeCompare(right)); -} - -function normalizeSearchText(value: string): string { - return String(value || "").replace(/\s+/g, " ").trim().toLowerCase(); -} - -function extractRoutingTerms(requestEntry: LauncherRequest): string[] { - const tagTerms = requestEntry.tags - .map((entry) => String(entry || "").trim()) - .filter(Boolean); - if (tagTerms.length > 0) { - return tagTerms.slice(0, 6); - } - const seen = new Set(); - const stopWords = new Set([ - "the", "and", "for", "with", "that", "this", "from", "into", "have", "need", - "want", "make", "more", "just", "like", "does", "dont", "cannot", "should", - "would", "could", "about", "because", "there", "their", "they", "them", "then", - "than", "over", "under", "your", "while", "where", - ]); - const matches = `${requestEntry.title} ${requestEntry.sourceText}`.match(/[A-Za-z][A-Za-z0-9/-]{2,}/g) || []; - return matches +function normalizeCommaSeparatedList(value: string): string[] { + return value + .split(",") .map((entry) => entry.trim()) - .filter((entry) => { - const normalized = entry.toLowerCase(); - if (stopWords.has(normalized) || seen.has(normalized)) { - return false; - } - seen.add(normalized); - return true; - }) - .slice(0, 6); -} - -function buildRoutingSummaryFallback(requestEntry: LauncherRequest, firstItem: LauncherRequestAnalysisItem | null): string { - const normalizedSummary = String(requestEntry.summary || "").trim(); - if (normalizedSummary && normalizedSummary !== "Awaiting parsing and categorization.") { - return normalizedSummary; - } - if (firstItem?.parsedInterpretation) { - return String(firstItem.parsedInterpretation).trim(); - } - const normalizedSource = String(requestEntry.sourceText || "").replace(/\s+/g, " ").trim(); - if (normalizedSource) { - return normalizedSource.length > 220 - ? `${normalizedSource.slice(0, 217).trim()}...` - : normalizedSource; - } - return "No routing summary has been stored yet."; -} - -function buildFallbackRouting(requestEntry: LauncherRequest): LauncherRequestAnalysisRouting { - const firstItem = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis.items[0] : null; - const routedTags = Array.isArray(firstItem?.tags) && firstItem.tags.length > 0 - ? firstItem.tags - : requestEntry.tags.length > 0 - ? requestEntry.tags - : (requestEntry.category && requestEntry.category !== "Unsorted" ? [requestEntry.category] : []); - const likelySystems = Array.isArray(firstItem?.affectedSystems) && firstItem.affectedSystems.length > 0 - ? firstItem.affectedSystems - : (routedTags.length > 0 ? routedTags : []); - const possibleDirections = Array.isArray(firstItem?.reviewOptions) && firstItem.reviewOptions.length > 0 - ? firstItem.reviewOptions - : (requestEntry.implementationNotes.trim() ? [requestEntry.implementationNotes.trim()] : []); - return { - summary: buildRoutingSummaryFallback(requestEntry, firstItem), - ambiguity: requestEntry.status === "pending" ? "medium" : "low", - matchedTerms: extractRoutingTerms(requestEntry), - suggestedTags: Array.isArray(routedTags) ? routedTags : [], - suggestedSystems: likelySystems, - suggestedModules: [], - rationale: String( - firstItem?.reviewRationale - || requestEntry.implementationNotes - || `Routing was reconstructed from the saved request title, tags, and submission text for "${requestEntry.title}".` - ).trim(), - possibleDirections, - kbSections: [], - }; -} - -function mergeRoutingWithFallback( - requestEntry: LauncherRequest, - existingRouting: LauncherRequestAnalysisRouting | undefined, -): LauncherRequestAnalysisRouting { - const fallbackRouting = buildFallbackRouting(requestEntry); - const normalizedRouting = existingRouting || {}; - return { - summary: String(normalizedRouting.summary || "").trim() || fallbackRouting.summary, - ambiguity: normalizedRouting.ambiguity || fallbackRouting.ambiguity, - matchedTerms: Array.isArray(normalizedRouting.matchedTerms) && normalizedRouting.matchedTerms.length > 0 - ? normalizedRouting.matchedTerms - : fallbackRouting.matchedTerms, - suggestedTags: Array.isArray(normalizedRouting.suggestedTags) && normalizedRouting.suggestedTags.length > 0 - ? normalizedRouting.suggestedTags - : fallbackRouting.suggestedTags, - suggestedSystems: Array.isArray(normalizedRouting.suggestedSystems) && normalizedRouting.suggestedSystems.length > 0 - ? normalizedRouting.suggestedSystems - : fallbackRouting.suggestedSystems, - suggestedModules: Array.isArray(normalizedRouting.suggestedModules) && normalizedRouting.suggestedModules.length > 0 - ? normalizedRouting.suggestedModules - : fallbackRouting.suggestedModules, - rationale: String(normalizedRouting.rationale || "").trim() || fallbackRouting.rationale, - possibleDirections: Array.isArray(normalizedRouting.possibleDirections) && normalizedRouting.possibleDirections.length > 0 - ? normalizedRouting.possibleDirections - : fallbackRouting.possibleDirections, - kbSections: Array.isArray(normalizedRouting.kbSections) && normalizedRouting.kbSections.length > 0 - ? normalizedRouting.kbSections - : fallbackRouting.kbSections, - }; -} - -function hydrateLauncherRequestForUi(requestEntry: LauncherRequest): LauncherRequest { - const nextRequest = cloneLauncherRequest(requestEntry); - nextRequest.analysis = { - ...(nextRequest.analysis || {}), - createdAt: nextRequest.analysis?.createdAt || nextRequest.createdAt, - updatedAt: nextRequest.analysis?.updatedAt || nextRequest.updatedAt, - itemCount: nextRequest.analysis?.itemCount ?? (Array.isArray(nextRequest.analysis?.items) ? nextRequest.analysis?.items.length : 0), - items: Array.isArray(nextRequest.analysis?.items) ? nextRequest.analysis.items : [], - routing: mergeRoutingWithFallback(nextRequest, nextRequest.analysis?.routing), - }; - return nextRequest; -} - -function buildRequestSearchCorpus(requestEntry: LauncherRequest): string { - return normalizeSearchText([ - requestEntry.title, - requestEntry.category, - requestEntry.tags.join(" "), - requestEntry.sourceText, - requestEntry.summary, - requestEntry.implementationNotes, - requestEntry.analysis?.routing?.summary, - requestEntry.analysis?.routing?.rationale, - ...(Array.isArray(requestEntry.analysis?.routing?.matchedTerms) ? requestEntry.analysis?.routing?.matchedTerms : []), - ...(Array.isArray(requestEntry.analysis?.items) - ? requestEntry.analysis.items.flatMap((item) => [ - item.title, - item.primaryCategory, - item.parsedInterpretation, - item.implementationApproach, - ...(Array.isArray(item.tags) ? item.tags : []), - ]) - : []), - ].filter(Boolean).join(" ")); -} - -function matchesRequestFilterToken(requestEntry: LauncherRequest, token: string): boolean { - if (token === "pending") { - return requestEntry.status === "pending"; - } - if (token === "queued") { - return isQueuedPendingRequest(requestEntry); - } - if (token === "review") { - return isNeedsReviewRequest(requestEntry); - } - if (token === "active") { - return requestEntry.status === "active"; - } - if (token === "implemented") { - return requestEntry.status === "implemented"; - } - return false; -} - -function requestMatchesFilters( - requestEntry: LauncherRequest, - searchText: string, - statusSelections: string[], - tagSelections: string[], -): boolean { - const normalizedSearchText = normalizeSearchText(searchText); - if (normalizedSearchText && !buildRequestSearchCorpus(requestEntry).includes(normalizedSearchText)) { - return false; - } - if (statusSelections.length > 0 && !statusSelections.some((token) => matchesRequestFilterToken(requestEntry, token))) { - return false; - } - if (tagSelections.length > 0 && !tagSelections.some((tag) => requestEntry.tags.includes(tag))) { - return false; - } - return true; -} - -function FilterIcon() { - return ( - - ); -} - -function LogsIcon() { - return ( - - ); + .filter(Boolean); } function SaveIcon() { @@ -400,70 +178,6 @@ function PlayIcon() { ); } -type LauncherChipSelectorProps = { - label: string; - values: string[]; - options: string[]; - placeholder: string; - emptyLabel?: string; - onAdd: (value: string) => void; - onRemove: (value: string) => void; -}; - -function LauncherChipSelector({ - label, - values, - options, - placeholder, - emptyLabel = "No tags selected yet.", - onAdd, - onRemove, -}: LauncherChipSelectorProps) { - const availableOptions = options.filter((option) => !values.includes(option)); - return ( -
-
- {label} - -
-
- {values.length > 0 ? values.map((value) => ( - - {value} - - - )) : ( - {emptyLabel} - )} -
-
- ); -} - async function resolveDefaultWorldId(): Promise { const response = await fetch("/api/world-default"); if (!response.ok) { @@ -674,6 +388,7 @@ function WorldshaperLauncher() { const launcherWindowMode = readLauncherWindowMode(); const adminWindowMode = launcherWindowMode === "admin"; const [launchState, setLaunchState] = useState("ready"); + const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window."); const [error, setError] = useState(""); const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK); const [activeBoardTab, setActiveBoardTab] = useState(adminWindowMode ? "requests" : "news"); @@ -684,10 +399,7 @@ function WorldshaperLauncher() { 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([]); - const [requestTagFilters, setRequestTagFilters] = useState([]); + const [requestFilter, setRequestFilter] = useState("all"); const [allowedRequestTags, setAllowedRequestTags] = useState([]); const [expandedRequestIds, setExpandedRequestIds] = useState([]); const [adminAccessGranted, setAdminAccessGranted] = useState(false); @@ -697,16 +409,10 @@ function WorldshaperLauncher() { 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([]); - const [adminTagFilters, setAdminTagFilters] = useState([]); const [adminEditorDraft, setAdminEditorDraft] = useState(null); - const [adminDetailTab, setAdminDetailTab] = useState("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">(""); @@ -741,7 +447,7 @@ function WorldshaperLauncher() { } try { const payload = await fetchJsonOrThrow("/api/launcher-requests"); - setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []); + setRequests(Array.isArray(payload.requests) ? payload.requests : []); setRequestsError(""); } catch (nextError: unknown) { setRequestsError(String(nextError || "Failed to load requests.")); @@ -821,7 +527,7 @@ function WorldshaperLauncher() { try { const payload = await fetchJsonOrThrow("/api/launcher-requests"); if (!cancelled) { - setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []); + setRequests(Array.isArray(payload.requests) ? payload.requests : []); } } catch { // Keep the current list visible during background refresh failures. @@ -862,38 +568,39 @@ function WorldshaperLauncher() { const selectedRequest = requests.find((entry) => entry.id === selectedAdminRequestId); if (selectedRequest) { if (!adminEditorDraft || adminEditorDraft.id !== selectedRequest.id) { - setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(selectedRequest))); + setAdminEditorDraft(cloneLauncherRequest(selectedRequest)); } return; } setSelectedAdminRequestId(requests[0].id); - setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(requests[0]))); + setAdminEditorDraft(cloneLauncherRequest(requests[0])); }, [adminPanelOpen, adminAccessGranted, requests, selectedAdminRequestId, adminEditorDraft]); useEffect(() => { setSelectedAdminAnalysisIndex(0); }, [selectedAdminRequestId]); - useEffect(() => { - setAdminDetailTab("routing"); - }, [selectedAdminRequestId]); - async function handleLaunch(): Promise { setError(""); const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK; setLaunchState("opening"); + setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`); try { const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK); setWorldId(resolvedWorldId); + setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`); if (openStudioPopup(resolvedWorldId)) { setLaunchState("opened"); + setStatus("Worldshaper Studio opened in a separate window."); return; } setLaunchState("blocked"); + setStatus("Your browser blocked the studio window. Use the launch button again after allowing popups."); } catch (nextError: unknown) { const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio."); setLaunchState("error"); setError(nextErrorText); + setStatus("Worldshaper Studio unavailable."); } } @@ -912,7 +619,7 @@ function WorldshaperLauncher() { }, body: JSON.stringify({ text }), }); - setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []); + setRequests(Array.isArray(payload.requests) ? payload.requests : []); setRequestDraft(""); setRequestDraftOpen(false); setRequestsError(""); @@ -969,7 +676,7 @@ function WorldshaperLauncher() { const nextRequest = requests.find((entry) => entry.id === requestId); setSelectedAdminRequestId(requestId); setSelectedAdminAnalysisIndex(0); - setAdminEditorDraft(nextRequest ? cloneLauncherRequest(hydrateLauncherRequestForUi(nextRequest)) : null); + setAdminEditorDraft(nextRequest ? cloneLauncherRequest(nextRequest) : null); setAdminNotice(""); setAdminPasswordError(""); } @@ -1035,10 +742,10 @@ function WorldshaperLauncher() { `/api/launcher-requests/${encodeURIComponent(adminEditorDraft.id)}`, buildAdminSavePayload(adminEditorDraft), ); - const nextRequests = Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : requests; + const nextRequests = Array.isArray(payload.requests) ? payload.requests : requests; setRequests(nextRequests); - const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || hydrateLauncherRequestForUi(payload.request || adminEditorDraft); - setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(refreshed))); + const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || payload.request || adminEditorDraft; + setAdminEditorDraft(cloneLauncherRequest(refreshed)); setAdminNotice(`Saved admin changes for "${adminEditorDraft.title}".`); if (adminPanelOpen) { void loadRecentSaveEvents(); @@ -1065,10 +772,6 @@ function WorldshaperLauncher() { 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", @@ -1095,11 +798,11 @@ function WorldshaperLauncher() { }), }, ); - const nextRequests = Array.isArray(promotePayload.requests) ? promotePayload.requests.map(hydrateLauncherRequestForUi) : []; + const nextRequests = Array.isArray(promotePayload.requests) ? promotePayload.requests : []; setRequests(nextRequests); const fallbackSelection = nextRequests[0] || null; setSelectedAdminRequestId(fallbackSelection?.id || ""); - setAdminEditorDraft(fallbackSelection ? cloneLauncherRequest(hydrateLauncherRequestForUi(fallbackSelection)) : null); + setAdminEditorDraft(fallbackSelection ? cloneLauncherRequest(fallbackSelection) : null); setAdminNotice(`Approved "${nextDraft.title}" and promoted its active request item${(nextDraft.analysis.items?.length || 0) === 1 ? "" : "s"}.`); if (adminPanelOpen) { void loadRecentSaveEvents(); @@ -1145,10 +848,10 @@ function WorldshaperLauncher() { }), }, ); - const nextRequests = Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : requests; + const nextRequests = Array.isArray(payload.requests) ? payload.requests : requests; setRequests(nextRequests); - const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || hydrateLauncherRequestForUi(payload.request || adminEditorDraft); - setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(refreshed))); + const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || payload.request || adminEditorDraft; + setAdminEditorDraft(cloneLauncherRequest(refreshed)); if (payload.launched) { setAdminNotice(mode === "draft" ? "Edited draft resubmitted to the analyzer." @@ -1198,7 +901,7 @@ function WorldshaperLauncher() { method: "DELETE", headers: buildAdminHeaders(adminPassword), }); - setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []); + setRequests(Array.isArray(payload.requests) ? payload.requests : []); setRequestsError(""); setExpandedRequestIds((current) => current.filter((entry) => entry !== requestEntry.id)); setAdminNotice(`Deleted request "${requestEntry.title}".`); @@ -1274,54 +977,38 @@ function WorldshaperLauncher() { const requestTagFilterOptions = requestTags .map((tag) => ({ tag, - count: requests.filter((entry) => entry.tags.includes(tag)).length, + count: requests.filter((entry) => entry.status !== "pending" && 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 filteredRequests = requests.filter((entry) => { + if (requestFilter === "status:pending") { + return entry.status === "pending"; + } + if (requestFilter === "status:queued") { + return isQueuedPendingRequest(entry); + } + if (requestFilter === "status:review") { + return isNeedsReviewRequest(entry); + } + if (requestFilter === "status:active") { + return entry.status === "active"; + } + if (requestFilter === "status:implemented") { + return entry.status === "implemented"; + } + if (requestFilter.startsWith("tag:")) { + const tag = requestFilter.slice(4); + return entry.status !== "pending" && entry.tags.includes(tag); + } + return true; + }); 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`); + const selectedAnalysisItem = adminEditorDraft?.analysis?.items?.[selectedAdminAnalysisIndex] || null; return (
- {launchState === "blocked" ?

Popup blocked. Allow popups, then press Launch again.

: null} - {error ?

{error}

: null} +
+

{status}

+ {launchState === "blocked" ? ( +

+ Allow the popup, then use Launch again to open the floating editor window. +

+ ) : null} + {launchState === "opened" ? ( +

+ The studio is open in its own slim window. This page stays behind as your release board and relaunch point. +

+ ) : null} + {launchState === "ready" ? ( +

+ The editor is designed to live in its own floating window, so the launcher keeps the first step clean. +

+ ) : null} + {error ?

{error}

: null} +
@@ -1410,10 +1114,9 @@ function WorldshaperLauncher() { ) : (
- {!adminWindowMode ? (
-
Shared Request Board
-
Requests
+
{adminWindowMode ? "Protected Review Workspace" : "Shared Request Board"}
+
{adminWindowMode ? "Admin Review Console" : "Requests"}
{requestCount} saved request{requestCount === 1 ? "" : "s"}: {queuedPendingRequestCount} queued, {needsReviewRequestCount} in review, {activeRequestCount} active, and {implementedRequestCount} implemented.
@@ -1427,7 +1130,7 @@ function WorldshaperLauncher() { }} disabled={requestSubmitting} > - {requestDraftOpen ? "Hide Request Form" : "Add New Request"} + {requestDraftOpen && !adminPanelOpen ? "Hide Request Form" : "Add New Request"} {!adminPanelOpen ? (
-
- setRequestSearchText(event.target.value)} - placeholder="Search requests..." - /> -
- - {requestFilterMenuOpen ? ( -
- {requestStatusFilterOptions.length > 0 ? ( -
-
Status
- {requestStatusFilterOptions.map((option) => ( - - ))} -
- ) : null} - {requestTagFilterOptions.length > 0 ? ( -
-
Tags
- {requestTagFilterOptions.map(({ tag, count }) => ( - - ))} -
- ) : null} -
- ) : null} -
-
+
- ) : null} {adminPanelOpen ? (
{!adminAccessGranted ? ( @@ -1535,35 +1201,13 @@ function WorldshaperLauncher() { ) : ( <> -
-
- - - +
+
+
Moderation Tools
+

Admin Panel

+

+ Run the VPS queue worker, review the latest request-analysis events, and manage deletions from one place. +

{queuedPendingRequestCount} queued @@ -1573,79 +1217,38 @@ function WorldshaperLauncher() { {implementedRequestCount} implemented
+
+ + +
{adminNotice ?

{adminNotice}

: null} {adminPasswordError ?

{adminPasswordError}

: null} {logsError ?

{logsError}

: null}
-
+

Request Management

Select a request to load it on the right.
-
- setAdminSearchText(event.target.value)} - placeholder="Search requests..." - /> -
- - {adminFilterMenuOpen ? ( -
- {requestStatusFilterOptions.length > 0 ? ( -
-
Status
- {requestStatusFilterOptions.map((option) => ( - - ))} -
- ) : null} - {requestTagFilterOptions.length > 0 ? ( -
-
Tags
- {requestTagFilterOptions.map(({ tag, count }) => ( - - ))} -
- ) : null} -
- ) : null} -
-
- {!requestsLoading && adminFilteredRequests.length === 0 ? ( -
No requests match the current search or filters.
- ) : null} - {adminFilteredRequests.map((requestEntry) => { + {requests.map((requestEntry) => { const isMutating = requestMutatingId === requestEntry.id; const isSelected = requestEntry.id === selectedAdminRequestId; - const requestDisplayState = getRequestDisplayStateLabel(requestEntry); - const requestDisplayStateClassName = getRequestDisplayStateClassName(requestEntry); return (
handleSelectAdminRequest(requestEntry.id)} >
-
-
{requestEntry.title}
-
- {requestDisplayState} -
-
+
{requestEntry.title}
+ {getRequestDisplayStateLabel(requestEntry)} {formatRequestTimestamp(requestEntry.updatedAt)}
{requestEntry.tags.length > 0 ? ( @@ -1688,6 +1287,29 @@ function WorldshaperLauncher() { })}
+
+
+

Recent Logs

+
Newest events first.
+
+
+ {logsLoading && recentSaveEvents.length === 0 ? ( +
Loading admin logs...
+ ) : null} + {!logsLoading && recentSaveEvents.length === 0 ? ( +
No admin logs have been recorded yet.
+ ) : null} + {recentSaveEvents.map((eventEntry, index) => ( +
+
+
{formatEventLabel(eventEntry)}
+
{formatRequestTimestamp(String(eventEntry.at || ""))}
+
+
{formatEventDetail(eventEntry) || "No extra details recorded."}
+
+ ))} +
+
{!adminEditorDraft ? ( @@ -1699,26 +1321,6 @@ function WorldshaperLauncher() {
Selected Request

{adminEditorDraft.title}

-
- - -
-
- {adminDetailTab === "routing" ? ( - <> -
-
-
Routing Pass
-
KB Routing Summary
-
-
-
- - -
-
- updateAdminDraft((current) => ({ ...current, tags: appendUniqueString(current.tags, value) }))} - onRemove={(value) => updateAdminDraft((current) => ({ ...current, tags: removeStringValue(current.tags, value) }))} - /> - updateAdminDraft((current) => ({ - ...current, - analysis: { - ...(current.analysis || {}), - routing: { - ...(current.analysis?.routing || {}), - suggestedTags: appendUniqueString( - Array.isArray(current.analysis?.routing?.suggestedTags) ? current.analysis.routing.suggestedTags : [], - value, - ), - }, - }, - }))} - onRemove={(value) => updateAdminDraft((current) => ({ - ...current, - analysis: { - ...(current.analysis || {}), - routing: { - ...(current.analysis?.routing || {}), - suggestedTags: removeStringValue( - Array.isArray(current.analysis?.routing?.suggestedTags) ? current.analysis.routing.suggestedTags : [], - value, - ), - }, - }, - }))} - /> +
+
+
+
+
Routing Pass
+
KB Routing Summary
+
+