import type { CSSProperties } from "react"; import { useEffect, useState } from "react"; import { openWorldshaperStudioWindow } from "./worldshaperStudio/windowing"; import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_FOOTNOTE, CHANGELOG_SPLASH_KICKER, CHANGELOG_SPLASH_TITLE, CHANGELOG_SPLASH_VERSION, } from "./worldshaperStudio/changelogData"; import type { ChangelogItem } from "./worldshaperStudio/changelogData"; import launcherBackground from "../background.png"; type WorldDefaultPayload = { worldId?: string; world?: { id?: string; }; }; type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error"; type BoardTab = "news" | "requests"; type LauncherRequest = { id: string; sourceSubmissionId?: string; title: string; status: "pending" | "active"; category: string; tags: string[]; sourceText: string; summary: string; implementationNotes: string; analysis?: { state?: "unprocessed" | "processing" | "processed" | "needs_review" | "error"; confidence?: number | null; model?: string; createdAt?: string; updatedAt?: string; error?: string; submissionId?: string; sourceTextSnapshot?: string; itemCount?: number; items?: Array<{ title?: string; primaryCategory?: string; tags?: string[]; statusRecommendation?: string; parsedInterpretation?: string; implementationApproach?: string; affectedSystems?: string[]; affectedFiles?: string[]; problemType?: string; rawExcerpt?: string; confidence?: number | null; notes?: string; }>; }; createdAt: string; updatedAt: string; }; type LauncherRequestsPayload = { requests?: LauncherRequest[]; }; type RecentSaveEvent = { at?: string; type?: string; requestId?: string; textPreview?: string; status?: string; category?: string; itemCount?: number; model?: string; reason?: string; provider?: string; pid?: number; code?: number | null; signal?: string; error?: string; }; type RecentSaveEventsPayload = { saves?: RecentSaveEvent[]; }; type ProcessPendingPayload = { ok?: boolean; launched?: boolean; reason?: string; autorunEnabled?: boolean; configured?: boolean; queuedPendingCount?: number; pid?: number; }; const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld"; async function resolveDefaultWorldId(): Promise { const response = await fetch("/api/world-default"); if (!response.ok) { throw new Error(`Failed to load default world (${response.status}).`); } const payload = await response.json() as WorldDefaultPayload; const resolvedWorldId = String(payload.worldId || payload.world?.id || "").trim(); return resolvedWorldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK; } async function fetchJsonOrThrow(input: RequestInfo | URL, init?: RequestInit): Promise { const response = await fetch(input, init); if (!response.ok) { let detail = `Request failed (${response.status}).`; try { const payload = await response.json() as { error?: string }; detail = String(payload?.error || detail); } catch { // Ignore JSON parse failures and fall back to status text. } throw new Error(detail); } return response.json() as Promise; } function formatRequestTimestamp(value: string): string { const parsed = Date.parse(String(value || "")); if (!Number.isFinite(parsed)) { return "Saved recently"; } return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }).format(parsed); } function formatRequestStatusLabel(status: "pending" | "active"): string { return status === "active" ? "Active" : "Pending"; } function normalizeAnalysisState(value: string | undefined): string { return String(value || "").trim().toLowerCase(); } function formatAnalysisStateLabel(value: string | undefined): string { const normalized = normalizeAnalysisState(value); if (normalized === "processing") { return "Processing"; } if (normalized === "processed") { return "Processed"; } if (normalized === "needs_review") { return "Needs Review"; } if (normalized === "error") { return "Error"; } return "Unprocessed"; } function getRequestDisplayStateLabel(requestEntry: LauncherRequest): string { if (requestEntry.status === "active") { return "Active"; } const analysisState = normalizeAnalysisState(requestEntry.analysis?.state); if (!analysisState || analysisState === "unprocessed") { return "Queued"; } if (analysisState === "needs_review") { return "Needs Review"; } if (analysisState === "error") { return "Analysis Error"; } if (analysisState === "processed") { return "Reviewed"; } return formatAnalysisStateLabel(analysisState); } function getRequestDisplayStateClassName(requestEntry: LauncherRequest): string { if (requestEntry.status === "active") { return "active"; } const analysisState = normalizeAnalysisState(requestEntry.analysis?.state); if (!analysisState || analysisState === "unprocessed") { return "queued"; } if (analysisState === "needs_review") { return "needs-review"; } if (analysisState === "error") { return "error"; } if (analysisState === "processed") { return "processed"; } if (analysisState === "processing") { return "processing"; } return "pending"; } function isQueuedPendingRequest(requestEntry: LauncherRequest): boolean { const analysisState = normalizeAnalysisState(requestEntry.analysis?.state); return requestEntry.status === "pending" && (!analysisState || analysisState === "unprocessed" || analysisState === "processing"); } function isNeedsReviewRequest(requestEntry: LauncherRequest): boolean { const analysisState = normalizeAnalysisState(requestEntry.analysis?.state); return requestEntry.status === "pending" && (analysisState === "needs_review" || analysisState === "error"); } function formatEventLabel(event: RecentSaveEvent): string { switch (String(event.type || "").trim()) { case "launcher-request-add": return "Request submitted"; case "launcher-request-delete": return "Request deleted"; case "launcher-request-update": return "Request updated"; case "launcher-request-review": return "Analysis saved for review"; case "launcher-request-promote": return "Pending request promoted"; case "launcher-request-analysis-error": return "Analysis failed"; case "launcher-request-analysis-launch": return "Queue worker launched"; case "launcher-request-analysis-finish": return "Queue worker finished"; case "launcher-request-analysis-launch-error": return "Queue worker launch error"; default: return String(event.type || "Event"); } } function formatEventDetail(event: RecentSaveEvent): string { const parts = [ event.requestId ? `Request ${event.requestId}` : "", event.category ? `Category ${event.category}` : "", event.status ? `Status ${event.status}` : "", event.itemCount ? `${event.itemCount} item${event.itemCount === 1 ? "" : "s"}` : "", event.provider ? `Provider ${event.provider}` : "", event.model ? `Model ${event.model}` : "", event.reason ? `Reason ${event.reason}` : "", event.pid ? `PID ${event.pid}` : "", Number.isFinite(Number(event.code)) ? `Exit ${event.code}` : "", event.signal ? `Signal ${event.signal}` : "", event.error ? String(event.error) : "", event.textPreview ? `Preview: ${event.textPreview}` : "", ].filter(Boolean); return parts.join(" • "); } function openStudioPopup(worldId: string): boolean { const popup = openWorldshaperStudioWindow(worldId, window, { worldId }); return Boolean(popup); } function openRepo(): void { window.location.assign("https://repo.andraxion.net/"); } function WorldshaperLauncher() { 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("news"); const [requests, setRequests] = useState([]); 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 [requestFilter, setRequestFilter] = useState("all"); const [expandedRequestIds, setExpandedRequestIds] = useState([]); const [adminPanelOpen, setAdminPanelOpen] = useState(false); const [recentSaveEvents, setRecentSaveEvents] = useState([]); const [logsLoading, setLogsLoading] = useState(false); const [logsError, setLogsError] = useState(""); const [queueTriggering, setQueueTriggering] = useState(false); const [adminNotice, setAdminNotice] = useState(""); useEffect(() => { let cancelled = false; void resolveDefaultWorldId() .then((resolvedWorldId) => { if (cancelled) { return; } setWorldId(resolvedWorldId); }) .catch(() => { if (cancelled) { return; } setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK); }); return () => { cancelled = true; }; }, []); async function loadRequests(options?: { silent?: boolean }): Promise { const silent = options?.silent === true; if (!silent) { setRequestsLoading(true); } try { const payload = await fetchJsonOrThrow("/api/launcher-requests"); setRequests(Array.isArray(payload.requests) ? payload.requests : []); setRequestsError(""); } catch (nextError: unknown) { setRequestsError(String(nextError || "Failed to load requests.")); } finally { if (!silent) { setRequestsLoading(false); } } } async function loadRecentSaveEvents(): Promise { setLogsLoading(true); try { const payload = await fetchJsonOrThrow("/api/debug/recent-saves"); setRecentSaveEvents(Array.isArray(payload.saves) ? payload.saves : []); setLogsError(""); } catch (nextError: unknown) { setLogsError(String(nextError || "Failed to load admin logs.")); } finally { setLogsLoading(false); } } async function refreshAdminData(options?: { includeLogs?: boolean; silentRequests?: boolean }): Promise { await loadRequests({ silent: options?.silentRequests === true }); if (options?.includeLogs) { await loadRecentSaveEvents(); } } useEffect(() => { void loadRequests(); }, []); useEffect(() => { if (!adminPanelOpen) { return; } void loadRecentSaveEvents(); }, [adminPanelOpen]); useEffect(() => { if (activeBoardTab !== "requests") { return; } let cancelled = false; const refreshBoard = async (): Promise => { try { const payload = await fetchJsonOrThrow("/api/launcher-requests"); if (!cancelled) { setRequests(Array.isArray(payload.requests) ? payload.requests : []); } } catch { // Keep the current list visible during background refresh failures. } if (!adminPanelOpen) { return; } try { const payload = await fetchJsonOrThrow("/api/debug/recent-saves"); 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]); 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."); } } async function handleAddRequest(): Promise { const text = requestDraft.trim(); if (!text) { setRequestsError("Write a request before saving it."); return; } setRequestSubmitting(true); try { const payload = await fetchJsonOrThrow("/api/launcher-requests", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text }), }); setRequests(Array.isArray(payload.requests) ? payload.requests : []); setRequestDraft(""); setRequestDraftOpen(false); setRequestsError(""); setAdminNotice("Request saved. The VPS queue worker will pick it up if analysis autorun is enabled."); if (adminPanelOpen) { void loadRecentSaveEvents(); } window.setTimeout(() => { void refreshAdminData({ includeLogs: adminPanelOpen, silentRequests: true }); }, 3500); } catch (nextError: unknown) { setRequestsError(String(nextError || "Failed to save request.")); } finally { setRequestSubmitting(false); } } function handleToggleExpandedRequest(requestId: string): void { setExpandedRequestIds((current) => ( current.includes(requestId) ? current.filter((entry) => entry !== requestId) : [...current, requestId] )); } async function handleDeleteRequest(requestEntry: LauncherRequest): Promise { const confirmed = window.confirm(`Delete this request?\n\n${requestEntry.title}`); if (!confirmed) { return; } setRequestMutatingId(requestEntry.id); try { const payload = await fetchJsonOrThrow(`/api/launcher-requests/${encodeURIComponent(requestEntry.id)}`, { method: "DELETE", }); setRequests(Array.isArray(payload.requests) ? payload.requests : []); setRequestsError(""); setExpandedRequestIds((current) => current.filter((entry) => entry !== requestEntry.id)); setAdminNotice(`Deleted request "${requestEntry.title}".`); if (adminPanelOpen) { void loadRecentSaveEvents(); } } catch (nextError: unknown) { setRequestsError(String(nextError || "Failed to delete request.")); } finally { setRequestMutatingId(""); } } async function handleProcessPendingQueue(): Promise { setQueueTriggering(true); try { const payload = await fetchJsonOrThrow("/api/launcher-requests/process-pending", { method: "POST", }); 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) { setLogsError(String(nextError || "Failed to trigger the queue worker.")); } finally { setQueueTriggering(false); } } const isBusy = launchState === "opening"; const requestCount = requests.length; const pendingRequestCount = requests.filter((entry) => entry.status === "pending").length; const activeRequestCount = requests.filter((entry) => entry.status === "active").length; const queuedPendingRequestCount = requests.filter(isQueuedPendingRequest).length; const needsReviewRequestCount = requests.filter(isNeedsReviewRequest).length; const requestTags = 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 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.startsWith("tag:")) { const tag = requestFilter.slice(4); return entry.status === "active" && entry.tags.includes(tag); } return true; }); const boardHint = activeBoardTab === "news" ? "Latest announcements" : `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`; return (
Worldshaper Studio
Floating editor launch

New RPG

Worldshaper Studio

{status}

{launchState === "blocked" ? (

Allow the popup, then use the studio button again to launch 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}
Worldshaper Board
{boardHint}
{activeBoardTab === "news" ? (
{CHANGELOG_SPLASH_KICKER}
{CHANGELOG_SPLASH_TITLE}
Release {CHANGELOG_SPLASH_VERSION}
{CHANGELOG_SECTIONS.map((section) => (

{section.title}

    {section.items.map((item, index) => { const key = `${section.title}-${index}`; const normalizedItem: ChangelogItem = item; if (typeof normalizedItem === "string") { return
  • {normalizedItem}
  • ; } return (
  • {normalizedItem.text}
    {normalizedItem.note ?
    {normalizedItem.note}
    : null}
  • ); })}
))}
{CHANGELOG_SPLASH_FOOTNOTE}
) : (
Shared Request Board
Requests
{requestCount} saved request{requestCount === 1 ? "" : "s"}: {queuedPendingRequestCount} queued, {needsReviewRequestCount} in review, and {activeRequestCount} active.
{adminPanelOpen ? (
Moderation Tools

Admin Panel

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

{queuedPendingRequestCount} queued {needsReviewRequestCount} review {pendingRequestCount} pending {activeRequestCount} active
{adminNotice ?

{adminNotice}

: null} {logsError ?

{logsError}

: null}

Request Management

Delete actions live here now.
{requests.map((requestEntry) => { const isMutating = requestMutatingId === requestEntry.id; const analysisState = formatAnalysisStateLabel(requestEntry.analysis?.state); return (
{requestEntry.title}
{formatRequestStatusLabel(requestEntry.status)} {requestEntry.category} {analysisState} {formatRequestTimestamp(requestEntry.updatedAt)}
); })}

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."}
))}
) : null} {requestDraftOpen ? (