diff --git a/app/api/support/linear/route.ts b/app/api/support/linear/route.ts
index c79f685..3a70051 100644
--- a/app/api/support/linear/route.ts
+++ b/app/api/support/linear/route.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import { admin, required } from "@/lib/supabase";
import { verifySupportWebhook } from "@/lib/support-signature";
import { supportCommand } from "@/shared/bug-reports";
+import { reconcileBugStatus, scheduleBugCleanup } from "@/lib/bug-lifecycle";
export const runtime = "nodejs";
export async function POST(request: NextRequest) {
@@ -59,6 +60,17 @@ export async function POST(request: NextRequest) {
.safeParse(decoded);
if (!payload.success) return new NextResponse(null, { status: 400 });
const event = payload.data;
+ if (event.type === "Issue" && ["create", "update"].includes(event.action)) {
+ try {
+ await reconcileBugStatus(event.data.id, true);
+ return NextResponse.json({ ok: true });
+ } catch {
+ return NextResponse.json(
+ { error: "Support status delivery failed" },
+ { status: 503 },
+ );
+ }
+ }
// Only explicit new staff comments are public. Internal comments and user echoes stay private.
if (
event.action !== "create" ||
@@ -71,6 +83,16 @@ export async function POST(request: NextRequest) {
const command = supportCommand(event.data.body ?? "");
if (!command) return NextResponse.json({ ok: true });
try {
+ if (command.kind === "close") {
+ required(
+ await admin().rpc("receive_bug_close", {
+ p_report: event.data.issueId,
+ p_external: event.data.id,
+ }),
+ );
+ await scheduleBugCleanup(event.data.issueId);
+ return NextResponse.json({ ok: true });
+ }
required(
await admin().rpc("receive_bug_reply", {
p_report: event.data.issueId,
diff --git a/app/dashboard/[id]/page.tsx b/app/dashboard/[id]/page.tsx
index 6e4c4ed..35332bf 100644
--- a/app/dashboard/[id]/page.tsx
+++ b/app/dashboard/[id]/page.tsx
@@ -2,6 +2,7 @@ import { getI18n } from "@/lib/i18n";
import { Header, Footer } from "@/components/Shell";
import { DraftEditor } from "@/components/DraftEditor";
import { user } from "@/lib/supabase";
+import { accountProfile } from "@/lib/profile";
import { redirect } from "next/navigation";
export default async function Draft({
params,
@@ -10,15 +11,17 @@ export default async function Draft({
}) {
const { t } = await getI18n();
const { id } = await params;
- if (!(await user()))
+ const account = await user();
+ if (!account)
redirect(`/log-in?next=${encodeURIComponent("/dashboard/" + id)}`);
+ const profile = await accountProfile(account);
return (
<>
{t("YOUR WORLD · YOUR LOCATIONS")}
{t("Map workshop")}
-
+
>
diff --git a/app/servers/[id]/page.tsx b/app/servers/[id]/page.tsx
index 34e106c..1d4e8ba 100644
--- a/app/servers/[id]/page.tsx
+++ b/app/servers/[id]/page.tsx
@@ -8,6 +8,7 @@ import { getMap } from "@/lib/catalog";
import { ContributionInstructions } from "@/components/ContributionInstructions";
import { ReportContent } from "@/components/ReportContent";
import { user } from "@/lib/supabase";
+import { accountProfile } from "@/lib/profile";
import { ExportMap } from "@/components/MapTransfer";
export default async function ServerPage({
params,
@@ -19,6 +20,7 @@ export default async function ServerPage({
const map = await getMap(id);
if (!map) notFound();
const account = await user();
+ const profile = account ? await accountProfile(account) : null;
return (
<>
@@ -63,7 +65,7 @@ export default async function ServerPage({
-
+
{map.allow_contributions && (
(null),
@@ -180,7 +186,9 @@ export function DraftEditor({ id }: { id: string }) {
"This shared map is private and playable without moderator review. Only the original owner can submit it for public review.",
)}
- {draft.state === "draft" && }
+ {draft.state === "draft" && (
+
+ )}
)}
{!draft.parent_dataset_id &&
diff --git a/components/StartGame.tsx b/components/StartGame.tsx
index 87fe4be..f77a025 100644
--- a/components/StartGame.tsx
+++ b/components/StartGame.tsx
@@ -2,16 +2,22 @@
import { useI18n } from "@/components/I18nProvider";
import { useState } from "react";
+import Link from "next/link";
import { useRouter } from "next/navigation";
import { ArrowRight, Timer, ZoomIn, Users } from "lucide-react";
import { difficulties, type Difficulty } from "@/shared/protocol";
import { api } from "@/lib/client-api";
export { api } from "@/lib/client-api";
-export function StartGame({ datasetId }: { datasetId: string }) {
+export function StartGame({
+ datasetId,
+ hostName,
+}: {
+ datasetId: string;
+ hostName: string | null;
+}) {
const { t } = useI18n();
const [difficulty, setDifficulty] = useState("normal"),
[mode, setMode] = useState<"solo" | "multiplayer">("solo"),
- [displayName, setDisplayName] = useState(""),
[playerLimit, setPlayerLimit] = useState(4),
[loading, setLoading] = useState(false),
[error, setError] = useState("");
@@ -25,7 +31,7 @@ export function StartGame({ datasetId }: { datasetId: string }) {
{
datasetId,
difficulty,
- ...(mode === "multiplayer" ? { displayName, playerLimit } : {}),
+ ...(mode === "multiplayer" ? { playerLimit } : {}),
},
);
router.push((mode === "solo" ? "/play/" : "/multiplayer/") + session.id);
@@ -82,17 +88,16 @@ export function StartGame({ datasetId }: { datasetId: string }) {
{mode === "multiplayer" && (
-
-
setDisplayName(event.target.value)}
- minLength={2}
- maxLength={32}
- placeholder={t("Enter a player name")}
- autoComplete="nickname"
- disabled={loading}
- />
+ {hostName ? (
+
+ {t("Host display name")}: {hostName}
+
+ ) : (
+
+ {t("Log in to create a multiplayer room")}{" "}
+ {t("Log in")}
+
+ )}
@@ -118,9 +123,7 @@ export function StartGame({ datasetId }: { datasetId: string }) {
- {report.state !== "draft" && (
+ {report.state !== "draft" && report.state !== "done" && (