Fix restricted map controls and release verification
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 5m17s

This commit is contained in:
2026-10-20 16:46:36 +05:00
parent de0e891b62
commit 690fe12c2c
8 changed files with 222 additions and 47 deletions

View File

@@ -91,19 +91,7 @@ jobs:
--connect-timeout 10 --max-time 30 \ --connect-timeout 10 --max-time 30 \
"$DOKPLOY_URL/api/deployment.all" "$DOKPLOY_URL/api/deployment.all"
previous_id="$(DOKPLOY_STATUS_FILE="$deployment_file" node <<'NODE' previous_ids="$(node scripts/dokploy-deployment-state.mjs snapshot "$deployment_file")" || {
const fs = require("node:fs");
let payload;
try { payload = JSON.parse(fs.readFileSync(process.env.DOKPLOY_STATUS_FILE, "utf8")); }
catch { console.error("Dokploy deployment list was not valid JSON."); process.exit(2); }
const rows = Array.isArray(payload)
? payload
: [payload?.data, payload?.deployments, payload?.items].find(Array.isArray);
if (!rows) process.exit(2);
const latest = rows[0];
process.stdout.write(String(latest?.id ?? latest?.deploymentId ?? ""));
NODE
)" || {
node scripts/dokploy-diagnostics.mjs request "API preflight response parsing" parse-error \ node scripts/dokploy-diagnostics.mjs request "API preflight response parsing" parse-error \
"$metrics_file" "$error_file" "$headers_file" "$deployment_file" "$auth_file" >&2 "$metrics_file" "$error_file" "$headers_file" "$deployment_file" "$auth_file" >&2
echo "Dokploy returned an unexpected deployment list; source was not uploaded." >&2 echo "Dokploy returned an unexpected deployment list; source was not uploaded." >&2
@@ -118,34 +106,25 @@ jobs:
echo "Dokploy accepted the source archive; waiting for the VPS build and deployment." echo "Dokploy accepted the source archive; waiting for the VPS build and deployment."
attempt=0 attempt=0
poll_failures=0
while [ "$attempt" -lt 360 ]; do while [ "$attempt" -lt 360 ]; do
dokploy_request "Deployment status GET /api/deployment.all (attempt $((attempt + 1))/360)" "$deployment_file" --get \ attempt=$((attempt + 1))
if dokploy_request "Deployment status GET /api/deployment.all (attempt $attempt/360)" "$deployment_file" --get \
--data-urlencode "applicationId=$DOKPLOY_APPLICATION_ID" \ --data-urlencode "applicationId=$DOKPLOY_APPLICATION_ID" \
--connect-timeout 10 --max-time 30 \ --connect-timeout 10 --max-time 30 \
"$DOKPLOY_URL/api/deployment.all" "$DOKPLOY_URL/api/deployment.all"; then
poll_failures=0
else
poll_failures=$((poll_failures + 1))
if node scripts/dokploy-deployment-state.mjs retry "$curl_exit" "$http_code" "$poll_failures"; then
echo "Transient status lookup failure ($poll_failures/6); the accepted build may still be running. Retrying only the GET."
sleep 10
continue
fi
exit 1
fi
status="$(DOKPLOY_STATUS_FILE="$deployment_file" DOKPLOY_PREVIOUS_DEPLOYMENT_ID="$previous_id" node <<'NODE' status="$(DOKPLOY_PREVIOUS_DEPLOYMENT_IDS="$previous_ids" node scripts/dokploy-deployment-state.mjs status "$deployment_file")" || {
const fs = require("node:fs");
let payload;
try { payload = JSON.parse(fs.readFileSync(process.env.DOKPLOY_STATUS_FILE, "utf8")); }
catch { console.error("Dokploy deployment status was not valid JSON."); process.exit(2); }
const rows = Array.isArray(payload)
? payload
: [payload?.data, payload?.deployments, payload?.items].find(Array.isArray);
if (!rows) process.exit(2);
const previousId = process.env.DOKPLOY_PREVIOUS_DEPLOYMENT_ID;
const current = rows.find((row) => {
const id = String(row?.id ?? row?.deploymentId ?? "");
return id && id !== previousId;
});
if (!current) {
process.stdout.write("waiting");
} else {
const value = String(current.status ?? "unknown").toLowerCase();
process.stdout.write(["done", "error", "running", "queued", "building", "deploying", "pending", "cancelled", "canceled"].includes(value) ? value : "unknown");
}
NODE
)" || {
node scripts/dokploy-diagnostics.mjs request "Deployment status response parsing" parse-error \ node scripts/dokploy-diagnostics.mjs request "Deployment status response parsing" parse-error \
"$metrics_file" "$error_file" "$headers_file" "$deployment_file" "$auth_file" >&2 "$metrics_file" "$error_file" "$headers_file" "$deployment_file" "$auth_file" >&2
echo "Dokploy returned an unexpected deployment status; inspect the response diagnostics above." >&2 echo "Dokploy returned an unexpected deployment status; inspect the response diagnostics above." >&2
@@ -169,7 +148,6 @@ jobs:
esac esac
sleep 10 sleep 10
attempt=$((attempt + 1))
done done
echo "Timed out waiting for Dokploy to finish; inspect the deployment in Dokploy." >&2 echo "Timed out waiting for Dokploy to finish; inspect the deployment in Dokploy." >&2

View File

@@ -66,10 +66,10 @@ export function GuessMap({
maxBounds: b.pad(0.15), maxBounds: b.pad(0.15),
attributionControl: false, attributionControl: false,
zoomControl: false, zoomControl: false,
scrollWheelZoom: zoom, scrollWheelZoom: zoom && (pan ? true : "center"),
touchZoom: zoom, touchZoom: zoom && (pan ? true : "center"),
doubleClickZoom: zoom, doubleClickZoom: zoom && (pan ? true : "center"),
boxZoom: zoom, boxZoom: zoom && pan,
dragging: pan, dragging: pan,
keyboard: false, keyboard: false,
}); });
@@ -232,6 +232,7 @@ export function GuessMap({
<div className="map-surface"> <div className="map-surface">
<div <div
ref={root} ref={root}
tabIndex={0}
style={{ height: "100%", width: "100%" }} style={{ height: "100%", width: "100%" }}
role="application" role="application"
aria-label={ aria-label={

View File

@@ -21,13 +21,31 @@ function inline(text: string) {
} }
export function ReleaseNotes({ body }: { body: string }) { export function ReleaseNotes({ body }: { body: string }) {
return body.split(/\r?\n\s*\r?\n/).map((block, i) => { const blocks: { list: boolean; lines: string[] }[] = [];
const lines = block.split(/\r?\n/); let current: (typeof blocks)[number] | undefined;
if (lines.every((line) => /^\s*[-*]\s+/.test(line))) for (const line of body.split(/\r?\n/)) {
if (!line.trim()) {
current = undefined;
continue;
}
const bullet = /^\s*[-*]\s+(.+)$/.exec(line);
if (!bullet && current?.list && /^\s{2,}\S/.test(line)) {
current.lines[current.lines.length - 1] += ` ${line.trim()}`;
continue;
}
const list = !!bullet;
if (!current || current.list !== list) {
current = { list, lines: [] };
blocks.push(current);
}
current.lines.push(bullet ? bullet[1] : line);
}
return blocks.map(({ list, lines }, i) => {
if (list)
return ( return (
<ul key={i}> <ul key={i}>
{lines.map((line, j) => ( {lines.map((line, j) => (
<li key={j}>{inline(line.replace(/^\s*[-*]\s+/, ""))}</li> <li key={j}>{inline(line)}</li>
))} ))}
</ul> </ul>
); );

View File

@@ -130,7 +130,12 @@ does not enable shell tracing, disable TLS verification, follow authenticated
redirects, or retry source uploads automatically. HTTP `000` means no HTTP redirects, or retry source uploads automatically. HTTP `000` means no HTTP
response was received; inspect the curl exit code and error immediately above it. response was received; inspect the curl exit code and error immediately above it.
The job waits for Dokploy's deployment status and fails if the upload or deployment The job waits for Dokploy's deployment status and fails if the upload or deployment
fails. Build logs remain available in the Dokploy dashboard. fails. Status polling tolerates up to six consecutive transient network/server
failures after an upload has been accepted, and retries only the read-only status
request. Authentication and certificate verification errors still stop the job.
All deployment IDs present before the upload are excluded from completion checks,
so an older successful build cannot be mistaken for the new release. Build logs
remain available in the Dokploy dashboard.
`git archive` sends tracked files from that commit only. It does not include `git archive` sends tracked files from that commit only. It does not include
`.git` metadata, local environment files, untracked files, or local caches. The `.git` metadata, local environment files, untracked files, or local caches. The

View File

@@ -0,0 +1,12 @@
# MineGeo 1.0.6 Beta — Void Update
The hardest expedition yet, more control over custom challenges, and release notes right on the site. 😄
- **Void difficulty:** Take on 10-second rounds with an End-purple glow, a fixed panorama and map, no zoom, compass, or coordinates, and strict scoring. Available in solo, multiplayer, and tournaments.
- **More custom settings:** Control guess-map panning and countdown visibility, and choose timers from 5 to 600 seconds in 5-second steps.
- **Changelogs:** Read release history on the new Changelogs page. It automatically reads release webhook posts from Discord news and removes role mentions and the play-site footer.
- **Control fixes:** Fixed keyboard access and Enter-to-pin on restricted maps. Disabling map panning now also blocks movement through zoom gestures.
- **Display fixes:** Improved release-note bullet formatting and kept navigation readable in English and Russian.
- **Deployment fixes:** Status checks tolerate temporary network failures and cannot mistake an older successful build for the new release.
PLAY THE MINEGEO AT: <https://minegeo.site/>

View File

@@ -0,0 +1,90 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
export function deploymentRows(payload) {
const rows = Array.isArray(payload)
? payload
: [payload?.data, payload?.deployments, payload?.items].find(Array.isArray);
if (!rows || rows.some((row) => !row || typeof row !== "object"))
throw new Error("Unexpected Dokploy deployment response");
return rows;
}
const identity = (row) => String(row.id ?? row.deploymentId ?? "");
export function knownDeploymentIds(payload) {
return deploymentRows(payload).map(identity).filter(Boolean);
}
export function deploymentState(payload, previousIds) {
if (
!Array.isArray(previousIds) ||
previousIds.some((id) => typeof id !== "string")
)
throw new Error("Invalid deployment baseline");
const known = new Set(previousIds);
const current = deploymentRows(payload).find(
(row) => identity(row) && !known.has(identity(row)),
);
if (!current) return "waiting";
const status = String(current.status ?? "unknown").toLowerCase();
return [
"done",
"error",
"running",
"queued",
"building",
"deploying",
"pending",
"cancelled",
"canceled",
].includes(status)
? status
: "unknown";
}
export function retryableStatusPoll(curlExit, httpCode, consecutiveFailures) {
if (
!Number.isInteger(consecutiveFailures) ||
consecutiveFailures < 1 ||
consecutiveFailures > 6
)
return false;
return (
[7, 28, 52, 55, 56].includes(curlExit) ||
(curlExit === 0 && [408, 429, 500, 502, 503, 504].includes(httpCode))
);
}
if (
process.argv[1] &&
fileURLToPath(import.meta.url) === resolve(process.argv[1])
) {
try {
const [mode, file, code, failures] = process.argv.slice(2);
if (mode === "retry") {
process.exitCode = retryableStatusPoll(
Number(file),
Number(code),
Number(failures),
)
? 0
: 1;
} else {
const payload = JSON.parse(readFileSync(file, "utf8"));
if (mode === "snapshot")
process.stdout.write(JSON.stringify(knownDeploymentIds(payload)));
else if (mode === "status")
process.stdout.write(
deploymentState(
payload,
JSON.parse(process.env.DOKPLOY_PREVIOUS_DEPLOYMENT_IDS ?? "null"),
),
);
else throw new Error("Unknown mode");
}
} catch {
console.error("Unexpected Dokploy deployment response or baseline.");
process.exitCode = 2;
}
}

View File

@@ -134,3 +134,20 @@ test("release Markdown renders emphasis and safe links, escaping executable HTML
assert.match(html, /href="https:\/\/minegeo.site\/docs"/); assert.match(html, /href="https:\/\/minegeo.site\/docs"/);
assert.doesNotMatch(html, /href="javascript:/); assert.doesNotMatch(html, /href="javascript:/);
}); });
test("release lists directly after a paragraph and wrapped bullet text remain lists", () => {
const html = renderToStaticMarkup(
createElement(ReleaseNotes, {
body: "This release improves:\n- **Controls:** Keyboard access\n and fixed map gestures.\n- Changelog lists.\nMore updates soon.",
}),
);
assert.match(html, /<p>This release improves:<\/p><ul>/);
assert.match(
html,
/<li><strong>Controls:<\/strong> Keyboard access and fixed map gestures\.<\/li>/,
);
assert.match(
html,
/<li>Changelog lists\.<\/li><\/ul><p>More updates soon\.<\/p>/,
);
});

View File

@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
deploymentState,
knownDeploymentIds,
retryableStatusPoll,
} from "../scripts/dokploy-deployment-state.mjs";
test("deployment monitoring cannot mistake any older successful build for the new upload", () => {
const baseline = [
{ id: "previous", status: "done" },
{ deploymentId: "older", status: "done" },
];
const ids = knownDeploymentIds({ deployments: baseline });
assert.equal(deploymentState(baseline, ids), "waiting");
assert.equal(deploymentState({ data: [baseline[1]] }, ids), "waiting");
assert.equal(
deploymentState([{ id: "new", status: "running" }, ...baseline], ids),
"running",
);
assert.equal(
deploymentState([{ id: "new", status: "done" }, ...baseline], ids),
"done",
);
assert.equal(
deploymentState([{ id: "new", status: "error" }, ...baseline], ids),
"error",
);
assert.equal(
deploymentState([{ id: "new", status: "cancelled" }], ids),
"cancelled",
);
});
test("deployment monitoring rejects malformed responses and baselines", () => {
assert.throws(() => knownDeploymentIds({ error: "Unauthorized" }));
assert.throws(() => deploymentState([null], []));
assert.throws(() => deploymentState([], "previous"));
assert.equal(
deploymentState([{ id: "first", status: "new-status" }], []),
"unknown",
);
});
test("only bounded transient read-only status failures retry; auth and TLS verification errors stop", () => {
for (const exit of [7, 28, 52, 55, 56])
assert.equal(retryableStatusPoll(exit, 0, 1), true);
for (const code of [408, 429, 500, 502, 503, 504])
assert.equal(retryableStatusPoll(0, code, 6), true);
for (const code of [400, 401, 403, 404])
assert.equal(retryableStatusPoll(0, code, 1), false);
assert.equal(retryableStatusPoll(60, 0, 1), false);
assert.equal(retryableStatusPoll(28, 0, 7), false);
});