diff --git a/app/api/version/route.ts b/app/api/version/route.ts
new file mode 100644
index 0000000..6c133d4
--- /dev/null
+++ b/app/api/version/route.ts
@@ -0,0 +1,16 @@
+import { NextResponse } from 'next/server'
+
+// BUILD_ID is set at build time by Next.js standalone output.
+// Falls back to a startup timestamp so every restart counts as a new version.
+const VERSION = process.env.BUILD_ID || process.env.NEXT_BUILD_ID || Date.now().toString()
+
+export function GET() {
+ return NextResponse.json(
+ { version: VERSION },
+ {
+ headers: {
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ },
+ }
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 7e183df..60c3fab 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,6 +3,7 @@ import type { Metadata } from "next"
import { Geist, Geist_Mono, Fredoka, Permanent_Marker } from "next/font/google"
import { MusicProvider } from "@/components/music/music-provider"
import { MiniPlayer } from "@/components/music/mini-player"
+import { UpdateBanner } from "@/components/update-banner"
import "./globals.css"
const _geist = Geist({ subsets: ["latin"] })
@@ -72,6 +73,7 @@ export default function RootLayout({
return (
+
{children}
diff --git a/components/update-banner.tsx b/components/update-banner.tsx
new file mode 100644
index 0000000..d9ac9a5
--- /dev/null
+++ b/components/update-banner.tsx
@@ -0,0 +1,54 @@
+'use client'
+
+import { useState, useEffect, useCallback } from 'react'
+import { RefreshCw } from 'lucide-react'
+
+const CHECK_INTERVAL = 60_000 // check every 60s
+
+export function UpdateBanner() {
+ const [updateAvailable, setUpdateAvailable] = useState(false)
+ const [initialVersion, setInitialVersion] = useState(null)
+
+ const checkVersion = useCallback(async () => {
+ try {
+ const res = await fetch('/api/version', { cache: 'no-store' })
+ const { version } = await res.json()
+ if (!version) return
+
+ if (initialVersion === null) {
+ setInitialVersion(version)
+ } else if (version !== initialVersion) {
+ setUpdateAvailable(true)
+ }
+ } catch {
+ // network error, skip
+ }
+ }, [initialVersion])
+
+ useEffect(() => {
+ checkVersion()
+ const id = setInterval(checkVersion, CHECK_INTERVAL)
+ return () => clearInterval(id)
+ }, [checkVersion])
+
+ // Also check on visibility change (user returns to tab)
+ useEffect(() => {
+ const onVisible = () => {
+ if (document.visibilityState === 'visible') checkVersion()
+ }
+ document.addEventListener('visibilitychange', onVisible)
+ return () => document.removeEventListener('visibilitychange', onVisible)
+ }, [checkVersion])
+
+ if (!updateAvailable) return null
+
+ return (
+
+ )
+}
diff --git a/next.config.mjs b/next.config.mjs
index fabed9b..6280284 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -7,6 +7,9 @@ const nextConfig = {
unoptimized: true,
},
output: 'standalone',
+ env: {
+ BUILD_ID: process.env.BUILD_ID || new Date().toISOString(),
+ },
async redirects() {
return [
{