diff --git a/next.config.mjs b/next.config.mjs index 5588e26..add6fdb 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,28 @@ +import { execSync } from 'node:child_process'; + +// The commit this build was made from, for the debug footer on every page. +// The FULL sha: the footer shows the short form but copies this, so what +// lands in the clipboard is directly usable with git. Vercel's build +// container has no .git directory but provides the sha in an env var; local +// builds ask git; anything else shows 'unknown' rather than failing the +// build over a label. +function resolveCommitSha() { + const fromPlatform = process.env.VERCEL_GIT_COMMIT_SHA; + if (fromPlatform) return fromPlatform; + try { + return execSync('git rev-parse HEAD', { + stdio: ['ignore', 'pipe', 'ignore'], + }).toString().trim(); + } catch { + return 'unknown'; + } +} + /** @type {import('next').NextConfig} */ const nextConfig = { + env: { + NEXT_PUBLIC_COMMIT_SHA: resolveCommitSha(), + }, // `next dev` and `next build` both write to the output directory, so a build // started while a dev server is serving makes that server read manifests the // build has just replaced. Leaving this unset keeps production on `.next`, diff --git a/src/app/components/DebugFooter.js b/src/app/components/DebugFooter.js new file mode 100644 index 0000000..723871c --- /dev/null +++ b/src/app/components/DebugFooter.js @@ -0,0 +1,51 @@ +"use client"; + +import { useEffect, useRef, useState } from 'react'; + +// Build stamp rendered on every page: which commit is this deployment? +// +// Bottom-center, styled as a link; clicking copies the FULL sha (the label +// shows the short form). z-40 keeps it under dialogs (z-50) and under the +// game screen's action bar and map (z-500+), so on a phone mid-round it ducks +// behind the controls. The wrapper stays click-transparent; only the button +// itself takes the pointer. +export default function DebugFooter() { + const sha = process.env.NEXT_PUBLIC_COMMIT_SHA || 'unknown'; + const [copied, setCopied] = useState(false); + const resetTimerRef = useRef(null); + + useEffect(() => () => clearTimeout(resetTimerRef.current), []); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(sha); + setCopied(true); + clearTimeout(resetTimerRef.current); + resetTimerRef.current = setTimeout(() => setCopied(false), 1500); + } catch (error) { + // Clipboard access can be denied (insecure context, permissions); the + // label is still readable, so failing quietly beats an alert. + console.error('Could not copy commit sha:', error); + } + }; + + return ( +