From aec0641f87adb764c9732f3b22e1d4b66bf43bde Mon Sep 17 00:00:00 2001
From: pavel444-byte
Date: Sun, 4 Oct 2026 23:12:34 +0500
Subject: [PATCH] Add panorama previews and repair beta export rendering
---
README.md | 2 +-
app/globals.css | 45 ++++++++++
components/DraftEditor.tsx | 62 +++++++++-----
components/Moderation.tsx | 37 ++++++++-
components/Panorama.tsx | 17 ++--
components/PanoramaPreview.tsx | 122 +++++++++++++++++++++++++++
components/StartGame.tsx | 1 +
docs/moderation.md | 42 ++++++++++
docs/verification.md | 19 +++++
lib/api.ts | 93 +++++++++++++++++++--
package.json | 1 +
renderer/block-colors.mjs | 35 ++++++++
renderer/scene.js | 35 +++++++-
renderer/terrain-mesh.js | 58 ++++++++++---
scripts/assets.mjs | 6 +-
scripts/repair-panoramas.mjs | 147 +++++++++++++++++++++++++++++++++
shared/draft-selection.ts | 16 ++++
shared/protocol.ts | 23 +++++-
tests/beta-regressions.test.ts | 91 ++++++++++++++++++++
tests/renderer-assets.test.ts | 3 +
worker/main.ts | 7 +-
21 files changed, 802 insertions(+), 60 deletions(-)
create mode 100644 components/PanoramaPreview.tsx
create mode 100644 docs/moderation.md
create mode 100644 renderer/block-colors.mjs
create mode 100644 scripts/repair-panoramas.mjs
create mode 100644 shared/draft-selection.ts
create mode 100644 tests/beta-regressions.test.ts
diff --git a/README.md b/README.md
index e3f1eb5..9050f61 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ npm start
Missing vanilla assets are also prepared automatically when a plugin export is rendered. For an existing failed or empty draft, stop the worker and use `npm run reprocess -- ` before restarting. This retains the raw upload. See [empty-render recovery](docs/server-plugin.md#recover-an-empty-render).
-`GET /api/v1/health` checks database connectivity. An empty catalog means no map has passed moderator review yet. Moderator access is controlled by `profiles.role` in Supabase; the project owner can set their profile's role to `moderator` using the database dashboard, then use `/moderation` on the website.
+`GET /api/v1/health` checks database connectivity. An empty catalog means no map has passed moderator review yet. Moderator access is controlled by `profiles.role` in Supabase; follow [moderator setup and the first beta map](docs/moderation.md). Contributors and moderators can inspect all six panorama faces before submission or approval, and saved draft selections retain excluded locations.
## Install the capture tools
diff --git a/app/globals.css b/app/globals.css
index 1330597..b65de3b 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1455,3 +1455,48 @@ pre {
font-size: 42px;
}
}
+/* Contributor and moderator previews use the same projection as the game. */
+.panorama-preview {
+ width: min(1100px, 96vw);
+ max-width: 96vw;
+ padding: 16px;
+ background: #0b1119;
+ color: #edf2ef;
+ border: 1px solid #58684b;
+}
+.panorama-preview::backdrop {
+ background: #000b;
+}
+.preview-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-bottom: 12px;
+}
+.preview-toolbar select {
+ background: #141e27;
+ color: inherit;
+ padding: 8px;
+}
+.preview-viewport {
+ position: relative;
+ height: min(65vh, 650px);
+ overflow: hidden;
+}
+.preview-status {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-content: center;
+ background: #0b1119e8;
+ z-index: 5;
+ padding: 20px;
+}
+.preview-tile {
+ min-width: 0;
+}
+.preview-tile > label {
+ display: block;
+ position: relative;
+}
diff --git a/components/DraftEditor.tsx b/components/DraftEditor.tsx
index 79e5da0..6507b17 100644
--- a/components/DraftEditor.tsx
+++ b/components/DraftEditor.tsx
@@ -5,6 +5,7 @@ import { api } from "./StartGame";
import type { DatasetManifest } from "@/shared/protocol";
import type { ProcessingJob } from "@/shared/processing-progress";
import { ProcessingProgress } from "./ProcessingProgress";
+import { PanoramaPreview } from "./PanoramaPreview";
type Draft = {
id: string;
name: string;
@@ -25,6 +26,7 @@ export function DraftEditor({ id }: { id: string }) {
[error, setError] = useState(""),
[message, setMessage] = useState(""),
[busy, setBusy] = useState(false);
+ const [preview, setPreview] = useState(null);
useEffect(() => {
let disposed = false;
let timer: ReturnType;
@@ -55,6 +57,7 @@ export function DraftEditor({ id }: { id: string }) {
async function save(submit = false) {
setBusy(true);
setError("");
+ setMessage("");
try {
await api(
"datasets/" + id,
@@ -147,32 +150,47 @@ export function DraftEditor({ id }: { id: string }) {
{draft.previews.map((p) => (
-
+
))}
+ {preview && (
+ setPreview(null)}
+ />
+ )}
{editable && (