Worldshaper/src/WorldshaperLauncher.tsx

1933 lines
95 KiB
TypeScript
Raw Normal View History

2026-06-26 21:24:08 -04:00
import type { CSSProperties } from "react";
import { useEffect, useState } from "react";
2026-06-26 21:36:41 -04:00
import { openWorldshaperStudioWindow } from "./worldshaperStudio/windowing";
import {
2026-06-26 21:36:41 -04:00
CHANGELOG_SECTIONS,
CHANGELOG_SPLASH_FOOTNOTE,
CHANGELOG_SPLASH_KICKER,
CHANGELOG_SPLASH_TITLE,
CHANGELOG_SPLASH_VERSION,
} from "./worldshaperStudio/changelogData";
import type { ChangelogItem } from "./worldshaperStudio/changelogData";
2026-06-26 21:24:08 -04:00
import launcherBackground from "../background.png";
type WorldDefaultPayload = {
worldId?: string;
world?: {
id?: string;
};
};
2026-06-26 21:36:41 -04:00
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
2026-06-26 22:55:50 -04:00
type BoardTab = "news" | "requests";
2026-06-27 01:44:11 -04:00
type LauncherWindowMode = "public" | "admin";
type LauncherRequestAnalysisRouting = {
summary?: string;
ambiguity?: "low" | "medium" | "high";
matchedTerms?: string[];
suggestedTags?: string[];
suggestedSystems?: string[];
suggestedModules?: string[];
rationale?: string;
possibleDirections?: string[];
kbSections?: string[];
};
2026-06-26 22:55:50 -04:00
type LauncherRequest = {
id: string;
sourceSubmissionId?: string;
title: string;
status: "pending" | "active";
category: string;
tags: string[];
sourceText: string;
summary: string;
implementationNotes: string;
2026-06-27 00:38:21 -04:00
analysis?: {
state?: "unprocessed" | "processing" | "processed" | "needs_review" | "error";
confidence?: number | null;
model?: string;
createdAt?: string;
updatedAt?: string;
error?: string;
submissionId?: string;
sourceTextSnapshot?: string;
2026-06-27 01:44:11 -04:00
routing?: LauncherRequestAnalysisRouting;
2026-06-27 00:38:21 -04:00
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;
2026-06-27 01:12:35 -04:00
reviewRationale?: string;
reviewOptions?: string[];
2026-06-27 00:38:21 -04:00
notes?: string;
}>;
};
2026-06-26 22:55:50 -04:00
createdAt: string;
updatedAt: string;
};
2026-06-27 01:12:35 -04:00
type LauncherRequestAnalysisItem = NonNullable<NonNullable<LauncherRequest["analysis"]>["items"]>[number];
2026-06-26 22:55:50 -04:00
type LauncherRequestsPayload = {
requests?: LauncherRequest[];
};
2026-06-27 00:38:21 -04:00
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;
};
2026-06-27 01:44:11 -04:00
type RequeueAnalysisPayload = {
ok?: boolean;
launched?: boolean;
reason?: string;
request?: LauncherRequest;
requests?: LauncherRequest[];
requestId?: string;
queuedPendingCount?: number;
pid?: number;
};
2026-06-27 01:12:35 -04:00
type LauncherRequestMetaPayload = {
allowedTags?: string[];
};
2026-06-27 00:51:20 -04:00
type AdminAuthPayload = {
ok?: boolean;
accessGranted?: boolean;
adminConfigured?: boolean;
error?: string;
};
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
2026-06-27 01:44:11 -04:00
function readLauncherWindowMode(): LauncherWindowMode {
if (typeof window === "undefined") {
return "public";
}
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get("admin") === "requests" ? "admin" : "public";
}
async function resolveDefaultWorldId(): Promise<string> {
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;
}
2026-06-26 22:55:50 -04:00
async function fetchJsonOrThrow<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
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<T>;
}
2026-06-27 00:51:20 -04:00
function buildAdminHeaders(password: string, headers?: HeadersInit): HeadersInit {
const normalizedPassword = String(password || "").trim();
if (!normalizedPassword) {
return {
...(headers || {}),
};
}
return {
...(headers || {}),
"x-worldshaper-admin-password": normalizedPassword,
};
}
function isAdminAccessError(error: unknown): boolean {
const text = String(error || "").toLowerCase();
return text.includes("admin access denied")
|| text.includes("admin access is not configured");
}
2026-06-27 01:12:35 -04:00
function cloneLauncherRequest(requestEntry: LauncherRequest): LauncherRequest {
return JSON.parse(JSON.stringify(requestEntry)) as LauncherRequest;
}
function getPrimaryAnalysisItem(requestEntry: LauncherRequest): LauncherRequestAnalysisItem | null {
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis?.items : [];
return items.length > 0 ? items[0] : null;
}
function formatConfidence(value: number | null | undefined): string {
if (!Number.isFinite(Number(value))) {
return "Unscored";
}
return `${Math.round(Number(value) * 100)}%`;
}
function escapePopupHtml(value: string): string {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function openReviewDetailsPopup(requestEntry: LauncherRequest): void {
const popup = window.open("", `worldshaper-review-${requestEntry.id}`, "popup=yes,width=840,height=760,resizable=yes,scrollbars=yes");
if (!popup) {
return;
}
2026-06-27 01:44:11 -04:00
const routing = requestEntry.analysis?.routing;
2026-06-27 01:12:35 -04:00
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis.items : [];
const renderedItems = items.map((item, index) => {
const reviewOptions = Array.isArray(item?.reviewOptions) ? item.reviewOptions : [];
return `
<section class="review-card">
<div class="review-kicker">Review Item ${index + 1}</div>
<h2>${escapePopupHtml(String(item?.title || `Request ${index + 1}`))}</h2>
<div class="review-meta">
<span>${escapePopupHtml(String(item?.primaryCategory || "Unsorted"))}</span>
<span>${escapePopupHtml(String(item?.statusRecommendation || "needs_review"))}</span>
<span>${escapePopupHtml(formatConfidence(item?.confidence))}</span>
</div>
<div class="review-block">
<h3>Review Rationale</h3>
<p>${escapePopupHtml(String(item?.reviewRationale || "No structured review rationale was returned."))}</p>
</div>
<div class="review-block">
<h3>Parsed Interpretation</h3>
<p>${escapePopupHtml(String(item?.parsedInterpretation || ""))}</p>
</div>
<div class="review-block">
<h3>Implementation Approach</h3>
<p>${escapePopupHtml(String(item?.implementationApproach || ""))}</p>
</div>
<div class="review-block">
<h3>Possible Options</h3>
${reviewOptions.length > 0
? `<ul>${reviewOptions.map((option) => `<li>${escapePopupHtml(String(option || ""))}</li>`).join("")}</ul>`
: "<p>No structured options were returned.</p>"}
</div>
<div class="review-block">
<h3>Notes</h3>
<p>${escapePopupHtml(String(item?.notes || "No extra notes."))}</p>
</div>
</section>
`;
}).join("");
popup.document.open();
popup.document.write(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Worldshaper Review Details</title>
<style>
body { margin: 0; font-family: Georgia, "Segoe UI", sans-serif; background: #08111f; color: #e8f2ff; }
main { max-width: 920px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; }
.hero { padding: 18px 20px; border: 1px solid #365782; border-radius: 14px; background: linear-gradient(180deg, rgba(17,32,63,.96), rgba(10,19,38,.98)); }
.hero h1 { margin: 0 0 8px; font-size: 28px; }
.hero p { margin: 0; color: #b8cfee; line-height: 1.6; white-space: pre-wrap; }
.review-card { padding: 18px 20px; border: 1px solid #365782; border-radius: 14px; background: rgba(17,32,63,.88); display: grid; gap: 12px; }
.review-kicker { color: #ffd166; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.review-card h2, .review-block h3 { margin: 0; }
.review-meta { display: flex; flex-wrap: wrap; gap: 8px; }
.review-meta span { padding: 4px 8px; border: 1px solid #365782; border-radius: 999px; background: rgba(8,16,31,.75); font-size: 12px; }
.review-block { display: grid; gap: 6px; }
.review-block p, .review-block ul { margin: 0; color: #d7e7ff; line-height: 1.6; white-space: pre-wrap; }
.review-block ul { padding-left: 18px; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>${escapePopupHtml(requestEntry.title)}</h1>
<p>${escapePopupHtml(requestEntry.sourceText)}</p>
</section>
2026-06-27 01:44:11 -04:00
${routing ? `
<section class="review-card">
<div class="review-kicker">Routing Summary</div>
<h2>${escapePopupHtml(String(routing.summary || "KB routing context"))}</h2>
<div class="review-meta">
<span>Ambiguity: ${escapePopupHtml(String(routing.ambiguity || "medium"))}</span>
${(Array.isArray(routing.suggestedTags) ? routing.suggestedTags : []).map((tag) => `<span>${escapePopupHtml(tag)}</span>`).join("")}
</div>
<div class="review-block">
<h3>Rationale</h3>
<p>${escapePopupHtml(String(routing.rationale || "No routing rationale was stored."))}</p>
</div>
<div class="review-block">
<h3>Matched Terms</h3>
${(Array.isArray(routing.matchedTerms) && routing.matchedTerms.length > 0)
? `<ul>${routing.matchedTerms.map((term) => `<li>${escapePopupHtml(String(term || ""))}</li>`).join("")}</ul>`
: "<p>No explicit terminology matches were stored.</p>"}
</div>
<div class="review-block">
<h3>Likely Systems</h3>
${(Array.isArray(routing.suggestedSystems) && routing.suggestedSystems.length > 0)
? `<ul>${routing.suggestedSystems.map((systemId) => `<li>${escapePopupHtml(String(systemId || ""))}</li>`).join("")}</ul>`
: "<p>No likely systems were stored.</p>"}
</div>
<div class="review-block">
<h3>Possible Directions</h3>
${(Array.isArray(routing.possibleDirections) && routing.possibleDirections.length > 0)
? `<ul>${routing.possibleDirections.map((direction) => `<li>${escapePopupHtml(String(direction || ""))}</li>`).join("")}</ul>`
: "<p>No alternate directions were stored.</p>"}
</div>
</section>
` : ""}
2026-06-27 01:12:35 -04:00
${renderedItems || '<section class="review-card"><h2>No Review Items</h2><p>This request does not have structured review details yet.</p></section>'}
</main>
</body>
</html>`);
popup.document.close();
}
2026-06-26 22:55:50 -04:00
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";
}
2026-06-27 00:38:21 -04:00
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";
2026-06-27 01:44:11 -04:00
case "launcher-request-analysis-requeue":
return "Request requeued for review";
2026-06-27 00:38:21 -04:00
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);
}
2026-06-26 21:12:44 -04:00
function openRepo(): void {
window.location.assign("https://repo.andraxion.net/");
}
2026-06-27 01:44:11 -04:00
function openAdminPanelWindow(): boolean {
const nextUrl = new URL(window.location.href);
nextUrl.searchParams.set("admin", "requests");
nextUrl.searchParams.set("tab", "requests");
const popup = window.open(nextUrl.toString(), "worldshaper-admin-panel", "popup=yes,width=1620,height=980,resizable=yes,scrollbars=yes");
if (popup) {
popup.focus();
}
return Boolean(popup);
}
function WorldshaperLauncher() {
2026-06-27 01:44:11 -04:00
const launcherWindowMode = readLauncherWindowMode();
const adminWindowMode = launcherWindowMode === "admin";
2026-06-26 21:36:41 -04:00
const [launchState, setLaunchState] = useState<LaunchState>("ready");
const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window.");
const [error, setError] = useState("");
2026-06-26 21:36:41 -04:00
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
2026-06-27 01:44:11 -04:00
const [activeBoardTab, setActiveBoardTab] = useState<BoardTab>(adminWindowMode ? "requests" : "news");
2026-06-26 22:55:50 -04:00
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 [requestFilter, setRequestFilter] = useState("all");
2026-06-27 01:12:35 -04:00
const [allowedRequestTags, setAllowedRequestTags] = useState<string[]>([]);
const [expandedRequestIds, setExpandedRequestIds] = useState<string[]>([]);
2026-06-27 00:51:20 -04:00
const [adminAccessGranted, setAdminAccessGranted] = useState(false);
const [adminPassword, setAdminPassword] = useState("");
const [adminPasswordDraft, setAdminPasswordDraft] = useState("");
const [adminAuthSubmitting, setAdminAuthSubmitting] = useState(false);
const [adminPasswordError, setAdminPasswordError] = useState("");
2026-06-27 01:12:35 -04:00
const [selectedAdminRequestId, setSelectedAdminRequestId] = useState("");
const [adminEditorDraft, setAdminEditorDraft] = useState<LauncherRequest | null>(null);
const [adminSaving, setAdminSaving] = useState(false);
2026-06-27 00:38:21 -04:00
const [recentSaveEvents, setRecentSaveEvents] = useState<RecentSaveEvent[]>([]);
const [logsLoading, setLogsLoading] = useState(false);
const [logsError, setLogsError] = useState("");
const [queueTriggering, setQueueTriggering] = useState(false);
2026-06-27 01:44:11 -04:00
const [requeueingMode, setRequeueingMode] = useState<"" | "saved" | "draft">("");
2026-06-27 00:38:21 -04:00
const [adminNotice, setAdminNotice] = useState("");
2026-06-27 01:44:11 -04:00
const adminPanelOpen = adminWindowMode;
useEffect(() => {
let cancelled = false;
2026-06-26 21:36:41 -04:00
void resolveDefaultWorldId()
.then((resolvedWorldId) => {
if (cancelled) {
return;
}
setWorldId(resolvedWorldId);
2026-06-26 21:36:41 -04:00
})
.catch(() => {
if (cancelled) {
return;
}
2026-06-26 21:36:41 -04:00
setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
});
return () => {
cancelled = true;
};
}, []);
2026-06-27 00:38:21 -04:00
async function loadRequests(options?: { silent?: boolean }): Promise<void> {
const silent = options?.silent === true;
if (!silent) {
setRequestsLoading(true);
}
try {
const payload = await fetchJsonOrThrow<LauncherRequestsPayload>("/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);
}
}
}
2026-06-27 01:12:35 -04:00
async function loadRequestMeta(): Promise<void> {
try {
const payload = await fetchJsonOrThrow<LauncherRequestMetaPayload>("/api/launcher-request-meta");
setAllowedRequestTags(Array.isArray(payload.allowedTags) ? payload.allowedTags : []);
} catch {
setAllowedRequestTags([]);
}
}
2026-06-27 00:38:21 -04:00
async function loadRecentSaveEvents(): Promise<void> {
setLogsLoading(true);
try {
2026-06-27 00:51:20 -04:00
const payload = await fetchJsonOrThrow<RecentSaveEventsPayload>("/api/debug/recent-saves", {
headers: buildAdminHeaders(adminPassword),
});
2026-06-27 00:38:21 -04:00
setRecentSaveEvents(Array.isArray(payload.saves) ? payload.saves : []);
setLogsError("");
} catch (nextError: unknown) {
2026-06-27 00:51:20 -04:00
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
}
2026-06-27 00:38:21 -04:00
setLogsError(String(nextError || "Failed to load admin logs."));
} finally {
setLogsLoading(false);
}
}
2026-06-27 00:51:20 -04:00
async function verifyAdminPassword(password: string): Promise<void> {
const payload = await fetchJsonOrThrow<AdminAuthPayload>("/api/admin/auth-check", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ password }),
});
if (!payload.accessGranted) {
throw new Error(String(payload.error || "Admin access denied."));
}
}
2026-06-27 00:38:21 -04:00
async function refreshAdminData(options?: { includeLogs?: boolean; silentRequests?: boolean }): Promise<void> {
await loadRequests({ silent: options?.silentRequests === true });
2026-06-27 00:51:20 -04:00
if (options?.includeLogs && adminAccessGranted && adminPassword) {
2026-06-27 00:38:21 -04:00
await loadRecentSaveEvents();
}
}
2026-06-26 22:55:50 -04:00
useEffect(() => {
2026-06-27 00:38:21 -04:00
void loadRequests();
2026-06-27 01:12:35 -04:00
void loadRequestMeta();
2026-06-27 00:38:21 -04:00
}, []);
2026-06-26 22:55:50 -04:00
2026-06-27 00:38:21 -04:00
useEffect(() => {
2026-06-27 00:51:20 -04:00
if (!adminPanelOpen || !adminAccessGranted || !adminPassword) {
2026-06-27 00:38:21 -04:00
return;
}
void loadRecentSaveEvents();
2026-06-27 00:51:20 -04:00
}, [adminPanelOpen, adminAccessGranted, adminPassword]);
2026-06-27 00:38:21 -04:00
useEffect(() => {
if (activeBoardTab !== "requests") {
return;
}
let cancelled = false;
const refreshBoard = async (): Promise<void> => {
2026-06-27 01:12:35 -04:00
if (!adminPanelOpen) {
try {
const payload = await fetchJsonOrThrow<LauncherRequestsPayload>("/api/launcher-requests");
if (!cancelled) {
setRequests(Array.isArray(payload.requests) ? payload.requests : []);
}
} catch {
// Keep the current list visible during background refresh failures.
2026-06-26 22:55:50 -04:00
}
2026-06-27 00:38:21 -04:00
}
2026-06-27 00:51:20 -04:00
if (!adminPanelOpen || !adminAccessGranted || !adminPassword) {
2026-06-27 00:38:21 -04:00
return;
}
try {
2026-06-27 00:51:20 -04:00
const payload = await fetchJsonOrThrow<RecentSaveEventsPayload>("/api/debug/recent-saves", {
headers: buildAdminHeaders(adminPassword),
});
2026-06-26 22:55:50 -04:00
if (!cancelled) {
2026-06-27 00:38:21 -04:00
setRecentSaveEvents(Array.isArray(payload.saves) ? payload.saves : []);
2026-06-26 22:55:50 -04:00
}
2026-06-27 00:38:21 -04:00
} catch {
// Avoid surfacing noisy polling failures in the admin panel.
2026-06-26 22:55:50 -04:00
}
2026-06-27 00:38:21 -04:00
};
const intervalId = window.setInterval(() => {
void refreshBoard();
}, 15000);
2026-06-26 22:55:50 -04:00
return () => {
cancelled = true;
2026-06-27 00:38:21 -04:00
window.clearInterval(intervalId);
2026-06-26 22:55:50 -04:00
};
2026-06-27 00:51:20 -04:00
}, [activeBoardTab, adminPanelOpen, adminAccessGranted, adminPassword]);
2026-06-26 22:55:50 -04:00
2026-06-27 01:12:35 -04:00
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(selectedRequest));
}
return;
}
setSelectedAdminRequestId(requests[0].id);
setAdminEditorDraft(cloneLauncherRequest(requests[0]));
}, [adminPanelOpen, adminAccessGranted, requests, selectedAdminRequestId, adminEditorDraft]);
2026-06-26 21:36:41 -04:00
async function handleLaunch(): Promise<void> {
setError("");
2026-06-26 21:36:41 -04:00
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`);
try {
2026-06-26 21:36:41 -04:00
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");
2026-06-26 21:36:41 -04:00
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.");
}
}
2026-06-26 22:55:50 -04:00
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 fetchJsonOrThrow<LauncherRequestsPayload>("/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("");
2026-06-27 00:38:21 -04:00
setAdminNotice("Request saved. The VPS queue worker will pick it up if analysis autorun is enabled.");
2026-06-27 00:51:20 -04:00
if (adminPanelOpen && adminAccessGranted) {
2026-06-27 00:38:21 -04:00
void loadRecentSaveEvents();
}
window.setTimeout(() => {
2026-06-27 00:51:20 -04:00
void refreshAdminData({ includeLogs: adminPanelOpen && adminAccessGranted, silentRequests: true });
2026-06-27 00:38:21 -04:00
}, 3500);
2026-06-26 22:55:50 -04:00
} catch (nextError: unknown) {
setRequestsError(String(nextError || "Failed to save request."));
} finally {
setRequestSubmitting(false);
}
}
2026-06-27 00:51:20 -04:00
async function handleAdminPanelToggle(): Promise<void> {
setRequestDraftOpen(false);
setRequestsError("");
setLogsError("");
2026-06-27 01:44:11 -04:00
if (adminWindowMode) {
2026-06-27 00:51:20 -04:00
return;
}
2026-06-27 01:44:11 -04:00
if (!openAdminPanelWindow()) {
setAdminNotice("Allow popups to open the admin review window.");
2026-06-27 00:51:20 -04:00
}
}
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);
}
}
2026-06-27 01:12:35 -04:00
function handleSelectAdminRequest(requestId: string): void {
const nextRequest = requests.find((entry) => entry.id === requestId);
setSelectedAdminRequestId(requestId);
setAdminEditorDraft(nextRequest ? cloneLauncherRequest(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;
});
}
function buildAdminSavePayload(requestEntry: LauncherRequest): RequestInit {
return {
method: "PATCH",
headers: buildAdminHeaders(adminPassword, {
"Content-Type": "application/json",
}),
body: JSON.stringify({
title: requestEntry.title,
status: requestEntry.status,
category: requestEntry.category,
tags: requestEntry.tags,
sourceText: requestEntry.sourceText,
summary: requestEntry.summary,
implementationNotes: requestEntry.implementationNotes,
analysis: requestEntry.analysis,
}),
};
}
async function handleSaveAdminRequest(): Promise<void> {
if (!adminEditorDraft) {
return;
}
setAdminSaving(true);
try {
const payload = await fetchJsonOrThrow<{ request?: LauncherRequest; requests?: LauncherRequest[] }>(
`/api/launcher-requests/${encodeURIComponent(adminEditorDraft.id)}`,
buildAdminSavePayload(adminEditorDraft),
);
const nextRequests = Array.isArray(payload.requests) ? payload.requests : requests;
setRequests(nextRequests);
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();
}
} 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 : [];
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 fetchJsonOrThrow<{ request?: LauncherRequest; requests?: LauncherRequest[] }>(
`/api/launcher-requests/${encodeURIComponent(nextDraft.id)}`,
buildAdminSavePayload(nextDraft),
);
const promotePayload = await fetchJsonOrThrow<LauncherRequestsPayload>(
`/api/launcher-requests/${encodeURIComponent(nextDraft.id)}/process-analysis`,
{
method: "POST",
headers: buildAdminHeaders(adminPassword, {
"Content-Type": "application/json",
}),
body: JSON.stringify({
action: "promote",
analysis: nextDraft.analysis,
}),
},
);
const nextRequests = Array.isArray(promotePayload.requests) ? promotePayload.requests : [];
setRequests(nextRequests);
const fallbackSelection = nextRequests[0] || null;
setSelectedAdminRequestId(fallbackSelection?.id || "");
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();
}
} 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);
}
}
2026-06-27 01:44:11 -04:00
async function handleRequeueAnalysis(mode: "saved" | "draft"): Promise<void> {
if (!adminEditorDraft) {
return;
}
setRequeueingMode(mode);
setLogsError("");
try {
const payload = await fetchJsonOrThrow<RequeueAnalysisPayload>(
`/api/launcher-requests/${encodeURIComponent(adminEditorDraft.id)}/requeue-analysis`,
{
method: "POST",
headers: buildAdminHeaders(adminPassword, {
"Content-Type": "application/json",
}),
body: JSON.stringify({
mode,
request: mode === "draft"
? {
title: adminEditorDraft.title,
category: adminEditorDraft.category,
tags: adminEditorDraft.tags,
sourceText: adminEditorDraft.sourceText,
summary: adminEditorDraft.summary,
implementationNotes: adminEditorDraft.implementationNotes,
}
: undefined,
}),
},
);
const nextRequests = Array.isArray(payload.requests) ? payload.requests : requests;
setRequests(nextRequests);
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."
: "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]
));
2026-06-26 22:55:50 -04:00
}
async function handleDeleteRequest(requestEntry: LauncherRequest): Promise<void> {
const confirmed = window.confirm(`Delete this request?\n\n${requestEntry.title}`);
2026-06-26 22:55:50 -04:00
if (!confirmed) {
return;
}
setRequestMutatingId(requestEntry.id);
try {
const payload = await fetchJsonOrThrow<LauncherRequestsPayload>(`/api/launcher-requests/${encodeURIComponent(requestEntry.id)}`, {
method: "DELETE",
2026-06-27 00:51:20 -04:00
headers: buildAdminHeaders(adminPassword),
2026-06-26 22:55:50 -04:00
});
setRequests(Array.isArray(payload.requests) ? payload.requests : []);
setRequestsError("");
setExpandedRequestIds((current) => current.filter((entry) => entry !== requestEntry.id));
2026-06-27 00:38:21 -04:00
setAdminNotice(`Deleted request "${requestEntry.title}".`);
if (adminPanelOpen) {
void loadRecentSaveEvents();
}
2026-06-26 22:55:50 -04:00
} catch (nextError: unknown) {
2026-06-27 00:51:20 -04:00
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
2026-06-26 22:55:50 -04:00
setRequestsError(String(nextError || "Failed to delete request."));
} finally {
setRequestMutatingId("");
}
}
2026-06-27 00:38:21 -04:00
async function handleProcessPendingQueue(): Promise<void> {
setQueueTriggering(true);
try {
const payload = await fetchJsonOrThrow<ProcessPendingPayload>("/api/launcher-requests/process-pending", {
method: "POST",
2026-06-27 00:51:20 -04:00
headers: buildAdminHeaders(adminPassword),
2026-06-27 00:38:21 -04:00
});
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) {
2026-06-27 00:51:20 -04:00
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
2026-06-27 00:38:21 -04:00
setLogsError(String(nextError || "Failed to trigger the queue worker."));
} finally {
setQueueTriggering(false);
}
}
2026-06-26 21:36:41 -04:00
const isBusy = launchState === "opening";
2026-06-26 22:55:50 -04:00
const requestCount = requests.length;
const pendingRequestCount = requests.filter((entry) => entry.status === "pending").length;
const activeRequestCount = requests.filter((entry) => entry.status === "active").length;
2026-06-27 00:38:21 -04:00
const queuedPendingRequestCount = requests.filter(isQueuedPendingRequest).length;
const needsReviewRequestCount = requests.filter(isNeedsReviewRequest).length;
2026-06-27 01:12:35 -04:00
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 filteredRequests = requests.filter((entry) => {
if (requestFilter === "status:pending") {
return entry.status === "pending";
}
2026-06-27 00:38:21 -04:00
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;
});
2026-06-27 01:44:11 -04:00
const boardTitle = adminWindowMode ? "Worldshaper Admin" : "Worldshaper Board";
const boardHint = adminWindowMode
? `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`
: (activeBoardTab === "news"
? "Latest announcements"
: `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`);
return (
2026-06-26 21:24:08 -04:00
<main
2026-06-27 01:44:11 -04:00
className={`launcher-shell ${adminWindowMode ? "launcher-shell-admin" : ""}`}
2026-06-26 21:24:08 -04:00
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
>
2026-06-27 01:44:11 -04:00
<div className={`launcher-stack ${adminWindowMode ? "launcher-stack-admin" : ""}`}>
{!adminWindowMode ? (
2026-06-26 22:01:43 -04:00
<section className="launcher-hero-window" aria-labelledby="launcher-studio-title">
<div className="launcher-hero-body">
2026-06-27 01:20:44 -04:00
<div className="launcher-hero-stack">
<div className="launcher-title-bubble">
2026-06-26 22:01:43 -04:00
<h1 className="launcher-title" id="launcher-studio-title">Worldshaper Studio</h1>
</div>
2026-06-27 01:20:44 -04:00
<div className="launcher-actions launcher-actions-floating">
<button type="button" className="launcher-primary-btn" onClick={() => void handleLaunch()} disabled={isBusy}>
Launch
</button>
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
Open Repo
</button>
</div>
<div className="launcher-hero-copy">
<p className="launcher-status">{status}</p>
2026-06-26 22:01:43 -04:00
{launchState === "blocked" ? (
<p className="launcher-hint">
2026-06-27 01:20:44 -04:00
Allow the popup, then use Launch again to open the floating editor window.
2026-06-26 22:01:43 -04:00
</p>
) : null}
{launchState === "opened" ? (
<p className="launcher-hint">
The studio is open in its own slim window. This page stays behind as your release board and relaunch point.
</p>
) : null}
{launchState === "ready" ? (
<p className="launcher-hint">
The editor is designed to live in its own floating window, so the launcher keeps the first step clean.
</p>
) : null}
{error ? <p className="launcher-error">{error}</p> : null}
</div>
</div>
</div>
2026-06-26 21:36:41 -04:00
</section>
2026-06-27 01:44:11 -04:00
) : null}
2026-06-26 22:55:50 -04:00
<section className="launcher-changelog-window" aria-labelledby="launcher-board-title">
2026-06-26 21:36:41 -04:00
<div className="launcher-changelog-titlebar">
2026-06-27 01:44:11 -04:00
<div className="launcher-changelog-title" id="launcher-board-title">{boardTitle}</div>
2026-06-26 22:55:50 -04:00
<div className="launcher-changelog-hint">{boardHint}</div>
2026-06-26 21:19:33 -04:00
</div>
2026-06-26 21:36:41 -04:00
<div className="launcher-changelog-body">
2026-06-26 22:55:50 -04:00
<div className="launcher-board-content">
2026-06-27 01:44:11 -04:00
{!adminWindowMode ? (
2026-06-26 22:55:50 -04:00
<div className="launcher-board-tabs">
<button
type="button"
className={`launcher-secondary-btn launcher-board-tab ${activeBoardTab === "news" ? "is-active" : ""}`}
onClick={() => setActiveBoardTab("news")}
>
News
</button>
<button
type="button"
className={`launcher-secondary-btn launcher-board-tab ${activeBoardTab === "requests" ? "is-active" : ""}`}
onClick={() => setActiveBoardTab("requests")}
>
Requests
</button>
2026-06-26 21:36:41 -04:00
</div>
2026-06-27 01:44:11 -04:00
) : null}
2026-06-26 22:55:50 -04:00
{activeBoardTab === "news" ? (
<div className="changelog-splash-card">
<div className="changelog-splash-hero">
<div className="changelog-splash-kicker">{CHANGELOG_SPLASH_KICKER}</div>
<div className="changelog-splash-title" id="launcher-whats-new-title">{CHANGELOG_SPLASH_TITLE}</div>
<div className="changelog-splash-meta">Release {CHANGELOG_SPLASH_VERSION}</div>
</div>
<div className="changelog-splash-list">
{CHANGELOG_SECTIONS.map((section) => (
<section key={section.title} className="changelog-splash-section">
<h3 className="changelog-splash-section-title">{section.title}</h3>
<ul className="changelog-splash-bullets">
{section.items.map((item, index) => {
const key = `${section.title}-${index}`;
const normalizedItem: ChangelogItem = item;
if (typeof normalizedItem === "string") {
return <li key={key}>{normalizedItem}</li>;
}
return (
<li key={key}>
<div>{normalizedItem.text}</div>
{normalizedItem.note ? <div className="changelog-splash-bullet-note">{normalizedItem.note}</div> : null}
</li>
);
})}
</ul>
</section>
))}
</div>
<div className="changelog-splash-footer">
<div className="changelog-splash-footnote">{CHANGELOG_SPLASH_FOOTNOTE}</div>
</div>
</div>
) : (
<div className="changelog-splash-card">
<div className="changelog-splash-hero">
2026-06-27 01:44:11 -04:00
<div className="changelog-splash-kicker">{adminWindowMode ? "Protected Review Workspace" : "Shared Request Board"}</div>
<div className="changelog-splash-title" id="launcher-requests-title">{adminWindowMode ? "Admin Review Console" : "Requests"}</div>
2026-06-26 22:55:50 -04:00
<div className="changelog-splash-meta">
2026-06-27 00:38:21 -04:00
{requestCount} saved request{requestCount === 1 ? "" : "s"}: {queuedPendingRequestCount} queued, {needsReviewRequestCount} in review, and {activeRequestCount} active.
2026-06-26 22:55:50 -04:00
</div>
2026-06-27 00:51:20 -04:00
<div className="launcher-request-hero-actions">
2026-06-27 00:38:21 -04:00
<div className="launcher-request-toolbar-buttons">
<button
type="button"
className="launcher-primary-btn"
2026-06-27 00:51:20 -04:00
onClick={() => {
setRequestDraftOpen((value) => !value);
}}
2026-06-27 00:38:21 -04:00
disabled={requestSubmitting}
>
2026-06-27 00:51:20 -04:00
{requestDraftOpen && !adminPanelOpen ? "Hide Request Form" : "Add New Request"}
2026-06-27 00:38:21 -04:00
</button>
2026-06-27 01:44:11 -04:00
{!adminPanelOpen ? (
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleAdminPanelToggle()}
>
Open Admin Window
</button>
) : null}
2026-06-27 00:38:21 -04:00
</div>
2026-06-27 00:51:20 -04:00
<label className="launcher-request-filter">
<span className="launcher-request-filter-label">Filter</span>
<select
className="launcher-request-filter-select"
value={requestFilter}
onChange={(event) => setRequestFilter(event.target.value)}
>
<option value="all">All Requests</option>
<option value="status:pending">Pending Requests</option>
<option value="status:queued">Queued For Analysis</option>
<option value="status:review">Needs Review</option>
<option value="status:active">Active Requests</option>
{requestTags.map((tag) => (
<option key={tag} value={`tag:${tag}`}>{tag}</option>
))}
</select>
</label>
</div>
2026-06-26 22:55:50 -04:00
</div>
2026-06-27 00:38:21 -04:00
{adminPanelOpen ? (
<section className="launcher-request-admin-panel">
2026-06-27 00:51:20 -04:00
{!adminAccessGranted ? (
<div className="launcher-request-admin-unlock">
<div className="launcher-request-admin-kicker">Protected Tools</div>
<h3 className="launcher-request-admin-title">Admin Access Required</h3>
2026-06-27 00:38:21 -04:00
<p className="launcher-request-admin-copy">
2026-06-27 00:51:20 -04:00
Enter the admin password to manage deletions, run the queue worker, and read request logs.
2026-06-27 00:38:21 -04:00
</p>
2026-06-27 00:51:20 -04:00
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Password</span>
<input
type="password"
className="launcher-request-filter-select"
value={adminPasswordDraft}
onChange={(event) => setAdminPasswordDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void handleAdminUnlock();
}
}}
placeholder="Enter admin password"
/>
</label>
<div className="launcher-request-admin-actions">
<button
type="button"
className="launcher-primary-btn"
onClick={() => void handleAdminUnlock()}
disabled={adminAuthSubmitting}
>
{adminAuthSubmitting ? "Unlocking..." : "Unlock Admin Panel"}
</button>
2026-06-27 00:38:21 -04:00
</div>
2026-06-27 00:51:20 -04:00
{adminPasswordError ? <p className="launcher-request-error">{adminPasswordError}</p> : null}
</div>
) : (
<>
<div className="launcher-request-admin-head">
<div>
<div className="launcher-request-admin-kicker">Moderation Tools</div>
<h3 className="launcher-request-admin-title">Admin Panel</h3>
<p className="launcher-request-admin-copy">
Run the VPS queue worker, review the latest request-analysis events, and manage deletions from one place.
</p>
</div>
<div className="launcher-request-admin-stats">
<span>{queuedPendingRequestCount} queued</span>
<span>{needsReviewRequestCount} review</span>
<span>{pendingRequestCount} pending</span>
<span>{activeRequestCount} active</span>
</div>
2026-06-27 00:38:21 -04:00
</div>
2026-06-27 00:51:20 -04:00
<div className="launcher-request-admin-actions">
<button
type="button"
className="launcher-primary-btn"
onClick={() => void handleProcessPendingQueue()}
disabled={queueTriggering}
>
{queueTriggering ? "Starting Queue..." : "Run Pending Queue"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void refreshAdminData({ includeLogs: true, silentRequests: true })}
disabled={logsLoading}
>
{logsLoading ? "Refreshing..." : "Refresh Admin Data"}
</button>
2026-06-27 00:38:21 -04:00
</div>
2026-06-27 00:51:20 -04:00
{adminNotice ? <p className="launcher-request-admin-notice">{adminNotice}</p> : null}
{adminPasswordError ? <p className="launcher-request-error">{adminPasswordError}</p> : null}
{logsError ? <p className="launcher-request-error">{logsError}</p> : null}
<div className="launcher-request-admin-grid">
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Request Management</h4>
2026-06-27 01:12:35 -04:00
<div className="launcher-request-admin-card-hint">Select a request to review or edit it.</div>
2026-06-27 00:51:20 -04:00
</div>
<div className="launcher-request-admin-request-list">
{requests.map((requestEntry) => {
const isMutating = requestMutatingId === requestEntry.id;
const analysisState = formatAnalysisStateLabel(requestEntry.analysis?.state);
2026-06-27 01:12:35 -04:00
const reviewItem = getPrimaryAnalysisItem(requestEntry);
const isSelected = requestEntry.id === selectedAdminRequestId;
2026-06-27 00:51:20 -04:00
return (
2026-06-27 01:12:35 -04:00
<article
key={`admin-${requestEntry.id}`}
className={`launcher-request-admin-request-row ${isSelected ? "is-selected" : ""}`}
onClick={() => handleSelectAdminRequest(requestEntry.id)}
>
2026-06-27 00:51:20 -04:00
<div className="launcher-request-admin-request-copy">
<div className="launcher-request-admin-request-title">{requestEntry.title}</div>
<div className="launcher-request-admin-request-meta">
<span>{formatRequestStatusLabel(requestEntry.status)}</span>
<span>{requestEntry.category}</span>
<span>{analysisState}</span>
2026-06-27 01:12:35 -04:00
<span>{formatConfidence(reviewItem?.confidence ?? requestEntry.analysis?.confidence)}</span>
2026-06-27 00:51:20 -04:00
<span>{formatRequestTimestamp(requestEntry.updatedAt)}</span>
</div>
2026-06-27 01:12:35 -04:00
{reviewItem?.reviewRationale ? (
<div className="launcher-request-admin-request-rationale">{reviewItem.reviewRationale}</div>
) : null}
2026-06-27 01:44:11 -04:00
{!reviewItem?.reviewRationale && requestEntry.analysis?.routing?.summary ? (
<div className="launcher-request-admin-request-rationale">{requestEntry.analysis.routing.summary}</div>
) : null}
2026-06-27 00:51:20 -04:00
</div>
<button
type="button"
className="launcher-request-delete-btn"
2026-06-27 01:12:35 -04:00
onClick={(event) => {
event.stopPropagation();
void handleDeleteRequest(requestEntry);
}}
2026-06-27 00:51:20 -04:00
disabled={isMutating}
aria-label={`Delete ${requestEntry.title}`}
>
X
</button>
</article>
);
})}
</div>
</section>
2026-06-27 01:12:35 -04:00
<section className="launcher-request-admin-card launcher-request-admin-editor-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Review Editor</h4>
<div className="launcher-request-admin-card-hint">Edit request fields, review details, and approval state.</div>
</div>
{!adminEditorDraft ? (
<div className="launcher-request-empty">Select a request from the list to review it.</div>
) : (
<div className="launcher-request-admin-editor">
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.title}
onChange={(event) => updateAdminDraft((current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Category</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.category}
onChange={(event) => updateAdminDraft((current) => ({ ...current, category: event.target.value }))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Original Submission</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={adminEditorDraft.sourceText}
onChange={(event) => updateAdminDraft((current) => ({ ...current, sourceText: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Summary</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.summary}
onChange={(event) => updateAdminDraft((current) => ({ ...current, summary: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Implementation Notes</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.implementationNotes}
onChange={(event) => updateAdminDraft((current) => ({ ...current, implementationNotes: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Tags</span>
<div className="launcher-request-admin-tag-grid">
{requestTags.map((tag) => {
const isActive = adminEditorDraft.tags.includes(tag);
return (
<button
key={`draft-tag-${tag}`}
type="button"
className={`launcher-request-tag launcher-request-admin-tag-toggle ${isActive ? "is-active" : ""}`}
onClick={() => updateAdminDraft((current) => ({
...current,
tags: isActive
? current.tags.filter((entry) => entry !== tag)
: [...current.tags, tag].sort((left, right) => left.localeCompare(right)),
}))}
>
{tag}
</button>
);
})}
</div>
</label>
2026-06-27 01:44:11 -04:00
<section className="launcher-request-admin-analysis-item">
<div className="launcher-request-admin-analysis-head">
<div>
<div className="launcher-request-admin-kicker">Routing Pass</div>
<div className="launcher-request-admin-request-title">KB Routing Summary</div>
</div>
</div>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Ambiguity</span>
<select
className="launcher-request-filter-select"
value={String(adminEditorDraft.analysis?.routing?.ambiguity || "medium")}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
ambiguity: event.target.value as "low" | "medium" | "high",
},
},
}))}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Suggested Tags</span>
<input
type="text"
className="launcher-request-filter-select"
value={Array.isArray(adminEditorDraft.analysis?.routing?.suggestedTags) ? adminEditorDraft.analysis?.routing?.suggestedTags?.join(", ") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
suggestedTags: event.target.value.split(",").map((entry) => entry.trim()).filter(Boolean),
},
},
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Routing Summary</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={String(adminEditorDraft.analysis?.routing?.summary || "")}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
summary: event.target.value,
},
},
}))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Routing Rationale</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={String(adminEditorDraft.analysis?.routing?.rationale || "")}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
rationale: event.target.value,
},
},
}))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Matched Terms</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(adminEditorDraft.analysis?.routing?.matchedTerms) ? adminEditorDraft.analysis?.routing?.matchedTerms?.join("\n") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
matchedTerms: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
},
},
}))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Likely Systems</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(adminEditorDraft.analysis?.routing?.suggestedSystems) ? adminEditorDraft.analysis?.routing?.suggestedSystems?.join("\n") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
suggestedSystems: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
},
},
}))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Possible Directions</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(adminEditorDraft.analysis?.routing?.possibleDirections) ? adminEditorDraft.analysis?.routing?.possibleDirections?.join("\n") : ""}
onChange={(event) => updateAdminDraft((current) => ({
...current,
analysis: {
...(current.analysis || {}),
routing: {
...(current.analysis?.routing || {}),
possibleDirections: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
},
},
}))}
/>
</label>
</section>
2026-06-27 01:12:35 -04:00
{(adminEditorDraft.analysis?.items || []).map((item, itemIndex) => (
<section key={`analysis-item-${itemIndex}`} className="launcher-request-admin-analysis-item">
<div className="launcher-request-admin-analysis-head">
<div>
<div className="launcher-request-admin-kicker">Review Item {itemIndex + 1}</div>
<div className="launcher-request-admin-request-title">{item.title || `Request ${itemIndex + 1}`}</div>
</div>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => openReviewDetailsPopup(adminEditorDraft)}
>
Open Review Popup
</button>
</div>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Item Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={String(item.title || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Primary Category</span>
<input
type="text"
className="launcher-request-filter-select"
value={String(item.primaryCategory || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, primaryCategory: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Recommendation</span>
<select
className="launcher-request-filter-select"
value={String(item.statusRecommendation || "needs_review")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, statusRecommendation: event.target.value }))}
>
<option value="needs_review">Needs Review</option>
<option value="active">Active</option>
<option value="blocked">Blocked</option>
<option value="duplicate">Duplicate</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Problem Type</span>
<select
className="launcher-request-filter-select"
value={String(item.problemType || "unknown")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, problemType: event.target.value }))}
>
<option value="feature">Feature</option>
<option value="bug">Bug</option>
<option value="workflow">Workflow</option>
<option value="performance">Performance</option>
<option value="ux">UX</option>
<option value="content">Content</option>
<option value="unknown">Unknown</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Confidence</span>
<input
type="number"
min="0"
max="1"
step="0.01"
className="launcher-request-filter-select"
value={Number.isFinite(Number(item.confidence)) ? String(item.confidence) : ""}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({
...current,
confidence: event.target.value === "" ? null : Number(event.target.value),
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Standardized Tags</span>
<div className="launcher-request-admin-tag-grid">
{requestTags.map((tag) => {
const isActive = Array.isArray(item.tags) && item.tags.includes(tag);
return (
<button
key={`item-${itemIndex}-tag-${tag}`}
type="button"
className={`launcher-request-tag launcher-request-admin-tag-toggle ${isActive ? "is-active" : ""}`}
onClick={() => updateAdminDraftItem(itemIndex, (current) => {
const currentTags = Array.isArray(current.tags) ? current.tags : [];
return {
...current,
tags: isActive
? currentTags.filter((entry) => entry !== tag)
: [...currentTags, tag].sort((left, right) => left.localeCompare(right)),
};
})}
>
{tag}
</button>
);
})}
</div>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Review Rationale</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.reviewRationale || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, reviewRationale: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Possible Options</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(item.reviewOptions) ? item.reviewOptions.join("\n") : ""}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({
...current,
reviewOptions: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
}))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Parsed Interpretation</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.parsedInterpretation || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, parsedInterpretation: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Implementation Approach</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.implementationApproach || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, implementationApproach: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Notes</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={String(item.notes || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, notes: event.target.value }))}
/>
</label>
</section>
))}
<div className="launcher-request-admin-actions">
<button
type="button"
className="launcher-primary-btn"
onClick={() => void handleSaveAdminRequest()}
2026-06-27 01:44:11 -04:00
disabled={adminSaving || requeueingMode !== ""}
2026-06-27 01:12:35 -04:00
>
{adminSaving ? "Saving..." : "Save Review Changes"}
</button>
2026-06-27 01:44:11 -04:00
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleRequeueAnalysis("saved")}
disabled={adminSaving || requeueingMode !== ""}
>
{requeueingMode === "saved" ? "Re-running Saved Review..." : "Review Saved Request"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleRequeueAnalysis("draft")}
disabled={adminSaving || requeueingMode !== ""}
>
{requeueingMode === "draft" ? "Submitting Draft Review..." : "Review Edited Draft"}
</button>
2026-06-27 01:12:35 -04:00
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleApproveAdminRequest()}
2026-06-27 01:44:11 -04:00
disabled={adminSaving || requeueingMode !== ""}
2026-06-27 01:12:35 -04:00
>
Approve Request
</button>
</div>
</div>
)}
</section>
2026-06-27 00:51:20 -04:00
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Recent Logs</h4>
<div className="launcher-request-admin-card-hint">Newest events first.</div>
</div>
<div className="launcher-request-admin-log-list">
{logsLoading && recentSaveEvents.length === 0 ? (
<div className="launcher-request-empty">Loading admin logs...</div>
) : null}
{!logsLoading && recentSaveEvents.length === 0 ? (
<div className="launcher-request-empty">No admin logs have been recorded yet.</div>
) : null}
{recentSaveEvents.map((eventEntry, index) => (
<article key={`log-${eventEntry.at || index}-${eventEntry.type || "event"}`} className="launcher-request-admin-log-row">
<div className="launcher-request-admin-log-head">
<div className="launcher-request-admin-log-title">{formatEventLabel(eventEntry)}</div>
<div className="launcher-request-admin-log-time">{formatRequestTimestamp(String(eventEntry.at || ""))}</div>
</div>
<div className="launcher-request-admin-log-detail">{formatEventDetail(eventEntry) || "No extra details recorded."}</div>
</article>
))}
</div>
</section>
2026-06-27 00:38:21 -04:00
</div>
2026-06-27 00:51:20 -04:00
</>
)}
2026-06-27 00:38:21 -04:00
</section>
) : null}
2026-06-27 00:51:20 -04:00
{requestDraftOpen && !adminPanelOpen ? (
2026-06-26 22:55:50 -04:00
<section className="launcher-request-composer">
<label className="launcher-request-composer-label" htmlFor="launcher-request-draft">
What should be added or improved?
</label>
<textarea
id="launcher-request-draft"
className="launcher-request-textarea"
value={requestDraft}
onChange={(event) => setRequestDraft(event.target.value)}
placeholder="Type a request for the board..."
maxLength={1000}
/>
<div className="launcher-request-composer-actions">
<button
type="button"
className="launcher-primary-btn"
onClick={() => void handleAddRequest()}
disabled={requestSubmitting}
>
{requestSubmitting ? "Saving Request..." : "Save Request"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => {
setRequestDraft("");
setRequestDraftOpen(false);
}}
disabled={requestSubmitting}
>
Cancel
</button>
</div>
</section>
) : null}
2026-06-27 00:51:20 -04:00
{!adminPanelOpen ? (
<div className="launcher-request-list">
2026-06-26 22:55:50 -04:00
{requestsLoading ? (
<section className="launcher-request-entry">
<div className="launcher-request-empty">Loading saved requests...</div>
</section>
) : null}
{!requestsLoading && filteredRequests.length === 0 ? (
2026-06-26 22:55:50 -04:00
<section className="launcher-request-entry">
<div className="launcher-request-empty">
{requests.length === 0
? "No requests yet. Add the first one to start the board."
: "No requests match the current filter."}
</div>
2026-06-26 22:55:50 -04:00
</section>
) : null}
{!requestsLoading ? filteredRequests.map((requestEntry) => {
const isExpanded = expandedRequestIds.includes(requestEntry.id);
const isActiveRequest = requestEntry.status === "active";
2026-06-27 00:38:21 -04:00
const requestDisplayState = getRequestDisplayStateLabel(requestEntry);
const requestDisplayStateClassName = getRequestDisplayStateClassName(requestEntry);
2026-06-27 01:12:35 -04:00
const reviewItem = getPrimaryAnalysisItem(requestEntry);
2026-06-27 00:38:21 -04:00
const analysisStateLabel = requestEntry.status === "pending"
? formatAnalysisStateLabel(requestEntry.analysis?.state)
: "";
2026-06-26 22:55:50 -04:00
return (
<section
key={requestEntry.id}
className={`launcher-request-entry is-${requestEntry.status} ${isExpanded ? "is-expanded" : ""} ${isActiveRequest ? "is-clickable" : ""}`}
onClick={isActiveRequest ? () => handleToggleExpandedRequest(requestEntry.id) : undefined}
2026-06-26 22:55:50 -04:00
>
<div className="launcher-request-entry-head">
<div className="launcher-request-entry-head-main">
2026-06-27 00:38:21 -04:00
<div className={`launcher-request-status-pill is-${requestDisplayStateClassName}`}>
{requestDisplayState}
</div>
<div className="launcher-request-entry-title-block">
<h3 className="launcher-request-entry-title">{requestEntry.title}</h3>
<div className="launcher-request-entry-category">{requestEntry.category}</div>
</div>
</div>
2026-06-26 22:55:50 -04:00
</div>
{requestEntry.tags.length > 0 ? (
<div className="launcher-request-tags">
{requestEntry.tags.map((tag) => (
<span key={`${requestEntry.id}-${tag}`} className="launcher-request-tag">{tag}</span>
))}
</div>
) : null}
<div className="launcher-request-entry-text">
{requestEntry.status === "active" ? requestEntry.summary : requestEntry.sourceText}
</div>
2026-06-27 01:12:35 -04:00
{requestEntry.status === "pending" && reviewItem?.reviewRationale ? (
<div className="launcher-request-entry-review-note">
Review reason: {reviewItem.reviewRationale}
</div>
) : null}
2026-06-27 00:38:21 -04:00
<div className="launcher-request-entry-meta">
{requestEntry.status === "pending" ? `${analysisStateLabel} | ` : ""}
{formatRequestTimestamp(requestEntry.updatedAt || requestEntry.createdAt)}
</div>
{isActiveRequest && isExpanded ? (
<div className="launcher-request-expanded">
<div className="launcher-request-expanded-block">
<div className="launcher-request-expanded-label">Parsed interpretation</div>
<p className="launcher-request-expanded-copy">{requestEntry.summary}</p>
</div>
<div className="launcher-request-expanded-block">
<div className="launcher-request-expanded-label">How we could do that</div>
<p className="launcher-request-expanded-copy">{requestEntry.implementationNotes}</p>
</div>
{requestEntry.sourceText ? (
<div className="launcher-request-expanded-block">
<div className="launcher-request-expanded-label">Original submission</div>
<p className="launcher-request-expanded-copy">{requestEntry.sourceText}</p>
</div>
) : null}
</div>
) : null}
2026-06-26 22:55:50 -04:00
</section>
);
}) : null}
2026-06-27 00:51:20 -04:00
</div>
) : null}
2026-06-26 22:55:50 -04:00
<div className="changelog-splash-footer">
<div className="changelog-splash-footnote">
2026-06-27 00:51:20 -04:00
Requests are saved and shared from this launcher. Public rows stay focused on the request itself, while moderation tools and logs stay behind protected admin access.
2026-06-26 22:55:50 -04:00
</div>
</div>
</div>
)}
{requestsError ? (
<p className="launcher-request-error">{requestsError}</p>
) : null}
2026-06-26 21:36:41 -04:00
</div>
2026-06-26 21:19:33 -04:00
</div>
</section>
2026-06-26 21:36:41 -04:00
</div>
</main>
);
}
export default WorldshaperLauncher;