diff --git a/src/app/(jury)/jury/page.tsx b/src/app/(jury)/jury/page.tsx
index ccd2da0..c113d99 100644
--- a/src/app/(jury)/jury/page.tsx
+++ b/src/app/(jury)/jury/page.tsx
@@ -3,6 +3,7 @@ import { Suspense } from 'react'
import Link from 'next/link'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
+import { AutoRefresh } from '@/components/shared/auto-refresh'
export const metadata: Metadata = { title: 'Jury Dashboard' }
export const dynamic = 'force-dynamic'
@@ -744,6 +745,9 @@ export default async function JuryDashboardPage() {
}>
+
+ {/* Auto-refresh every 30s so voting round changes appear promptly */}
+
)
}
diff --git a/src/components/shared/auto-refresh.tsx b/src/components/shared/auto-refresh.tsx
new file mode 100644
index 0000000..2299df1
--- /dev/null
+++ b/src/components/shared/auto-refresh.tsx
@@ -0,0 +1,42 @@
+'use client'
+
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+
+/**
+ * Invisible client component that periodically calls router.refresh()
+ * to re-fetch server component data without a full page reload.
+ * Pauses when the tab is hidden to avoid unnecessary requests.
+ */
+export function AutoRefresh({ intervalMs = 30_000 }: { intervalMs?: number }) {
+ const router = useRouter()
+
+ useEffect(() => {
+ let timer: ReturnType
+
+ function start() {
+ timer = setInterval(() => {
+ router.refresh()
+ }, intervalMs)
+ }
+
+ function handleVisibility() {
+ clearInterval(timer)
+ if (document.visibilityState === 'visible') {
+ // Refresh immediately when tab becomes visible again
+ router.refresh()
+ start()
+ }
+ }
+
+ start()
+ document.addEventListener('visibilitychange', handleVisibility)
+
+ return () => {
+ clearInterval(timer)
+ document.removeEventListener('visibilitychange', handleVisibility)
+ }
+ }, [router, intervalMs])
+
+ return null
+}