fix: add generation guard to refresh, preserve image on transient failure, document env vars

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 01:50:08 -04:00
parent 9ac236575c
commit 1cf2a855b5
2 changed files with 26 additions and 9 deletions

View File

@@ -137,6 +137,8 @@ The database will be automatically initialized and populated with games from `ga
DB_PATH=./data/jackbox.db DB_PATH=./data/jackbox.db
JWT_SECRET=your-secret-jwt-key JWT_SECRET=your-secret-jwt-key
ADMIN_KEY=admin123 ADMIN_KEY=admin123
VOTE_APP_URL=https://vote-app.example.com
VOTE_APP_API_KEY=your-vote-app-api-key
``` ```
4. **Start the backend server** 4. **Start the backend server**
@@ -184,6 +186,13 @@ If `admins.json` is not found, the app falls back to the `ADMIN_KEY` environment
To use a custom path for the admins file, set `ADMIN_CONFIG_PATH` in your environment. To use a custom path for the admins file, set `ADMIN_CONFIG_PATH` in your environment.
### Sticker Wall Preview
The backend proxies sticker wall images from the vote-app. Set these in `backend/.env`:
- `VOTE_APP_URL` — base URL of the vote-app (e.g. `https://vote-app.example.com`)
- `VOTE_APP_API_KEY` — API key for the sticker wall render endpoint
For Docker, uncomment the volume mount in `docker-compose.yml`: For Docker, uncomment the volume mount in `docker-compose.yml`:
```yaml ```yaml
- ./backend/config/admins.json:/app/config/admins.json:ro - ./backend/config/admins.json:/app/config/admins.json:ro

View File

@@ -6,6 +6,7 @@ export function useStickerWall(sessionId) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const urlRef = useRef(null); const urlRef = useRef(null);
const generationRef = useRef(0);
const revokeUrl = useCallback(() => { const revokeUrl = useCallback(() => {
if (urlRef.current) { if (urlRef.current) {
@@ -23,20 +24,28 @@ export function useStickerWall(sessionId) {
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
if (!sessionId) return; if (!sessionId) return;
const generation = generationRef.current;
setLoading(true); setLoading(true);
setError(false); setError(false);
try { try {
const res = await api.post(`/stickerwalls/${sessionId}/render`, null, { responseType: 'blob' }); const res = await api.post(`/stickerwalls/${sessionId}/render`, null, { responseType: 'blob' });
if (generation !== generationRef.current) return;
setBlobUrl(res.data); setBlobUrl(res.data);
} catch { } catch {
setError(true); if (generation !== generationRef.current) return;
if (!urlRef.current) {
setError(true);
}
} finally { } finally {
setLoading(false); if (generation === generationRef.current) {
setLoading(false);
}
} }
}, [sessionId, setBlobUrl]); }, [sessionId, setBlobUrl]);
useEffect(() => { useEffect(() => {
let stale = false; generationRef.current += 1;
const generation = generationRef.current;
if (!sessionId) { if (!sessionId) {
revokeUrl(); revokeUrl();
@@ -50,17 +59,17 @@ export function useStickerWall(sessionId) {
setError(false); setError(false);
try { try {
const res = await api.get(`/stickerwalls/${id}`, { responseType: 'blob' }); const res = await api.get(`/stickerwalls/${id}`, { responseType: 'blob' });
if (stale) return; if (generation !== generationRef.current) return;
setBlobUrl(res.data); setBlobUrl(res.data);
} catch (err) { } catch (err) {
if (stale) return; if (generation !== generationRef.current) return;
if (err.response?.status === 404) { if (err.response?.status === 404) {
try { try {
const renderRes = await api.post(`/stickerwalls/${id}/render`, null, { responseType: 'blob' }); const renderRes = await api.post(`/stickerwalls/${id}/render`, null, { responseType: 'blob' });
if (stale) return; if (generation !== generationRef.current) return;
setBlobUrl(renderRes.data); setBlobUrl(renderRes.data);
} catch { } catch {
if (stale) return; if (generation !== generationRef.current) return;
revokeUrl(); revokeUrl();
setImageUrl(null); setImageUrl(null);
setError(true); setError(true);
@@ -71,7 +80,7 @@ export function useStickerWall(sessionId) {
setError(true); setError(true);
} }
} finally { } finally {
if (!stale) { if (generation === generationRef.current) {
setLoading(false); setLoading(false);
} }
} }
@@ -80,7 +89,6 @@ export function useStickerWall(sessionId) {
fetchWall(sessionId); fetchWall(sessionId);
return () => { return () => {
stale = true;
revokeUrl(); revokeUrl();
}; };
}, [sessionId, setBlobUrl, revokeUrl]); }, [sessionId, setBlobUrl, revokeUrl]);