Skip to content

Commit 4bc7cfe

Browse files
authored
Merge pull request #31 from adn8naiagent/dev
1.2.1 - 2026-03-14
2 parents dd4ba57 + d587a74 commit 4bc7cfe

File tree

13 files changed

+167
-31
lines changed

13 files changed

+167
-31
lines changed

.github/workflows/publish-docker.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ jobs:
2424
context: ./backend
2525
- image: f1replaytiming-frontend
2626
context: ./frontend
27-
build-args: |
28-
NEXT_PUBLIC_API_URL=http://localhost:8000
2927

3028
steps:
3129
- name: Checkout

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to F1 Timing Replay will be documented in this file.
44

5+
## 1.2.1 - 2026-03-14
6+
7+
### Fixes
8+
- **Connection error screen** - when the frontend cannot reach the backend, a clear error message is now shown instead of the passphrase screen, with the attempted URL and troubleshooting tips
9+
- **Runtime API URL for Docker** - `NEXT_PUBLIC_API_URL` can now be set as a runtime environment variable on the frontend container, so pre-built Docker images work with any backend URL without rebuilding. See the README for details
10+
- Fixed loading overlay staying visible when navigating back to the session picker
11+
12+
### New Features
13+
- **Red flag countdown and skip** - during red flag periods in replay mode, a countdown timer shows how long until the session resumes, with a button to skip ahead to the restart
14+
15+
---
16+
517
## 1.2.0 - 2026-03-14
618

719
### New Features

README.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ services:
6060
image: ghcr.io/adn8naiagent/f1replaytiming-frontend:latest
6161
ports:
6262
- "3000:3000"
63+
environment:
64+
- NEXT_PUBLIC_API_URL=http://localhost:8000 # Change to your backend URL if not using localhost
6365
depends_on:
6466
- backend
6567

@@ -90,13 +92,47 @@ Open http://localhost:3000. Select any past session and it will be processed on
9092

9193
### Docker configuration
9294

93-
To enable optional features, edit the environment variables in `docker-compose.yml`:
95+
#### Network & URL configuration
96+
97+
Two environment variables control how the frontend and backend find each other:
98+
99+
| Variable | Set on | Purpose |
100+
|---|---|---|
101+
| `NEXT_PUBLIC_API_URL` | frontend | The URL your **browser** uses to reach the backend |
102+
| `FRONTEND_URL` | backend | The URL your browser uses to reach the frontend (needed for CORS) |
103+
104+
The defaults (`http://localhost:8000` and `http://localhost:3000`) work when accessing the app on the same machine running Docker. If you access from another device, use a reverse proxy, or change ports, update both variables to match.
105+
106+
**Example — accessing from other devices on your network:**
107+
```yaml
108+
backend:
109+
environment:
110+
- FRONTEND_URL=http://192.168.1.50:3000
111+
112+
frontend:
113+
environment:
114+
- NEXT_PUBLIC_API_URL=http://192.168.1.50:8000
115+
```
116+
117+
**Example — behind a reverse proxy (e.g. Cloudflare Tunnel, nginx):**
118+
```yaml
119+
backend:
120+
environment:
121+
- FRONTEND_URL=https://f1.example.com
122+
123+
frontend:
124+
environment:
125+
- NEXT_PUBLIC_API_URL=https://api.f1.example.com
126+
```
127+
128+
In this setup your reverse proxy routes `f1.example.com` to the frontend container (port 3000) and `api.f1.example.com` to the backend container (port 8000).
129+
130+
#### Optional features
131+
94132
- `OPENROUTER_API_KEY` - enables the photo sync feature ([get a key](https://openrouter.ai/))
95133
- `AUTH_ENABLED` / `AUTH_PASSPHRASE` - restricts access with a passphrase
96134

97-
If you change ports, make sure to update:
98-
- `FRONTEND_URL` on the backend to match the URL you access the frontend on (used for CORS)
99-
- `NEXT_PUBLIC_API_URL` on the frontend to match the backend URL (this is baked in at build time, so rebuild with `docker compose up --build` after changing it)
135+
#### Data
100136

101137
Session data is persisted in a Docker volume, so it survives restarts.
102138

backend/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ DATA_DIR=./data
1111
R2_ACCOUNT_ID=
1212
R2_ACCESS_KEY_ID=
1313
R2_SECRET_ACCESS_KEY=
14-
R2_BUCKET_NAME=f1timingdata
14+
R2_BUCKET_NAME=
1515

1616
# Only needed for pre-compute script (not required at runtime)
1717
FASTF1_CACHE_DIR=.fastf1-cache

backend/routers/auth_routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,10 @@ async def auth_login(body: LoginRequest):
2323
if token is None:
2424
raise HTTPException(status_code=401, detail="Invalid passphrase")
2525
return {"token": token}
26+
27+
28+
@router.get("/verify")
29+
async def auth_verify():
30+
"""Validate a cached token. Not in AUTH_SKIP_PATHS, so the auth middleware
31+
will reject invalid tokens with 401 before this handler runs."""
32+
return {"valid": True}

docker-compose.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ services:
44
ports:
55
- "8000:8000" # Change the left side to use a different host port
66
environment:
7-
- FRONTEND_URL=http://localhost:3000 # Must match the URL you access the frontend on (for CORS)
7+
- FRONTEND_URL=http://localhost:3000 # The URL your browser uses to access the frontend (for CORS)
88
- DATA_DIR=/data
99
# Optional - uncomment to enable features
1010
# - OPENROUTER_API_KEY=your-key-here # enables photo/screenshot sync (manual entry sync works without this)
@@ -15,14 +15,11 @@ services:
1515
- f1cache:/data/fastf1-cache
1616

1717
frontend:
18-
build:
19-
context: ./frontend
20-
args:
21-
# This URL is baked into the frontend at build time — it's how the browser reaches the backend.
22-
# If you change the backend port above, update this to match (e.g. http://localhost:9000).
23-
NEXT_PUBLIC_API_URL: http://localhost:8000
18+
build: ./frontend
2419
ports:
2520
- "3000:3000" # Change the left side to use a different host port
21+
environment:
22+
- NEXT_PUBLIC_API_URL=http://localhost:8000 # The URL your browser uses to reach the backend
2623
depends_on:
2724
- backend
2825

frontend/Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ WORKDIR /app
1212
COPY --from=deps /app/node_modules ./node_modules
1313
COPY . .
1414

15-
ARG NEXT_PUBLIC_API_URL
15+
ARG NEXT_PUBLIC_API_URL=__NEXT_PUBLIC_API_URL__
1616
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
1717

1818
RUN npm run build
@@ -30,11 +30,15 @@ COPY --from=builder /app/public ./public
3030
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
3131
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
3232

33+
COPY entrypoint.sh /entrypoint.sh
34+
RUN chmod +x /entrypoint.sh
35+
3336
USER nextjs
3437

3538
EXPOSE 3000
3639

3740
ENV PORT=3000
3841
ENV HOSTNAME="0.0.0.0"
3942

43+
ENTRYPOINT ["/entrypoint.sh"]
4044
CMD ["node", "server.js"]

frontend/entrypoint.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/sh
2+
# Replace the build-time placeholder with the runtime NEXT_PUBLIC_API_URL.
3+
# If NEXT_PUBLIC_API_URL is not set, default to http://localhost:8000.
4+
RUNTIME_URL="${NEXT_PUBLIC_API_URL:-http://localhost:8000}"
5+
PLACEHOLDER="__NEXT_PUBLIC_API_URL__"
6+
7+
if [ "$RUNTIME_URL" != "$PLACEHOLDER" ]; then
8+
find /app/.next -name "*.js" -exec sed -i "s|$PLACEHOLDER|$RUNTIME_URL|g" {} +
9+
echo "Configured API URL: $RUNTIME_URL"
10+
fi
11+
12+
exec "$@"

frontend/src/components/AuthGate.tsx

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,75 @@
11
"use client";
22

3-
import { useState, useEffect, FormEvent } from "react";
4-
import { apiUrl } from "@/lib/api";
5-
import { getToken, setToken } from "@/lib/auth";
3+
import { useState, useEffect, useCallback, FormEvent } from "react";
4+
import { apiUrl, API_URL } from "@/lib/api";
5+
import { getToken, setToken, clearToken } from "@/lib/auth";
66

77
export default function AuthGate({ children }: { children: React.ReactNode }) {
88
const [checking, setChecking] = useState(true);
99
const [authRequired, setAuthRequired] = useState(false);
1010
const [authenticated, setAuthenticated] = useState(false);
11+
const [connectionError, setConnectionError] = useState(false);
1112
const [passphrase, setPassphrase] = useState("");
1213
const [showPassphrase, setShowPassphrase] = useState(false);
1314
const [error, setError] = useState("");
1415
const [submitting, setSubmitting] = useState(false);
1516

16-
useEffect(() => {
17-
fetch(apiUrl("/api/auth/status"))
18-
.then((res) => res.json())
17+
const checkAuth = useCallback(() => {
18+
setChecking(true);
19+
setConnectionError(false);
20+
21+
const url = apiUrl("/api/auth/status");
22+
console.log(`[AuthGate] Checking auth status at ${url}`);
23+
24+
fetch(url)
25+
.then((res) => {
26+
if (!res.ok) {
27+
console.error(`[AuthGate] Auth status returned ${res.status}`);
28+
throw new Error(`Backend returned ${res.status}`);
29+
}
30+
return res.json();
31+
})
1932
.then((data) => {
33+
console.log(`[AuthGate] Auth enabled: ${data.auth_enabled}`);
2034
if (!data.auth_enabled) {
2135
setAuthenticated(true);
36+
setChecking(false);
2237
} else {
2338
setAuthRequired(true);
2439
// Check if we have a cached token that still works
2540
const token = getToken();
2641
if (token) {
27-
fetch(apiUrl("/api/health"), {
42+
fetch(apiUrl("/api/auth/verify"), {
2843
headers: { Authorization: `Bearer ${token}` },
2944
}).then((res) => {
3045
if (res.ok) {
46+
console.log("[AuthGate] Cached token is valid");
3147
setAuthenticated(true);
48+
} else {
49+
console.log("[AuthGate] Cached token is invalid, clearing");
50+
clearToken();
3251
}
3352
setChecking(false);
34-
}).catch(() => setChecking(false));
53+
}).catch(() => {
54+
console.error("[AuthGate] Failed to validate cached token");
55+
setChecking(false);
56+
});
3557
} else {
3658
setChecking(false);
3759
}
3860
}
3961
})
40-
.catch(() => {
41-
// Can't reach backend — assume auth is required so we don't bypass it
42-
setAuthRequired(true);
43-
})
44-
.finally(() => {
45-
if (!authRequired) setChecking(false);
62+
.catch((err) => {
63+
console.error(`[AuthGate] Cannot connect to backend at ${API_URL}:`, err.message);
64+
setConnectionError(true);
65+
setChecking(false);
4666
});
4767
}, []);
4868

69+
useEffect(() => {
70+
checkAuth();
71+
}, [checkAuth]);
72+
4973
const handleSubmit = async (e: FormEvent) => {
5074
e.preventDefault();
5175
setError("");
@@ -83,6 +107,50 @@ export default function AuthGate({ children }: { children: React.ReactNode }) {
83107
);
84108
}
85109

110+
if (connectionError) {
111+
return (
112+
<div className="min-h-screen bg-f1-dark flex items-center justify-center px-4">
113+
<div className="w-full max-w-md">
114+
<div className="text-center mb-8">
115+
<img src="/logo.png" alt="F1 Replay" className="w-16 h-16 rounded-lg mx-auto mb-4" />
116+
<h1 className="text-xl font-bold text-white">F1 Replay Timing</h1>
117+
</div>
118+
119+
<div className="bg-f1-card border border-f1-border rounded-xl p-6">
120+
<h2 className="text-sm font-bold text-red-400 mb-3">Cannot connect to backend</h2>
121+
<p className="text-sm text-f1-muted mb-3">
122+
The frontend failed to reach the API server at:
123+
</p>
124+
<code className="block text-xs text-white bg-f1-dark border border-f1-border rounded px-3 py-2 mb-4 break-all">
125+
{API_URL}
126+
</code>
127+
<div className="text-xs text-f1-muted space-y-2">
128+
<p>Common causes:</p>
129+
<ul className="list-disc list-inside space-y-1 ml-1">
130+
<li>The backend container is still starting up</li>
131+
<li>
132+
<code className="text-white">NEXT_PUBLIC_API_URL</code> in your
133+
docker-compose.yml is set to a URL that isn&apos;t reachable from
134+
your browser
135+
</li>
136+
<li>
137+
If behind a reverse proxy, this URL must be the address your browser
138+
uses to reach the backend, not an internal Docker address
139+
</li>
140+
</ul>
141+
</div>
142+
<button
143+
onClick={checkAuth}
144+
className="w-full mt-5 px-4 py-2 bg-f1-red text-white text-sm font-bold rounded hover:bg-red-700 transition-colors"
145+
>
146+
Retry
147+
</button>
148+
</div>
149+
</div>
150+
</div>
151+
);
152+
}
153+
86154
if (authenticated) {
87155
return <>{children}</>;
88156
}

frontend/src/components/SessionPicker.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export default function SessionPicker() {
122122
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
123123
const [menuOpen, setMenuOpen] = useState(false);
124124
const [navigating, setNavigating] = useState(false);
125+
useEffect(() => setNavigating(false), []);
125126
const latestRef = useRef<HTMLDivElement>(null);
126127
const menuRef = useRef<HTMLDivElement>(null);
127128

0 commit comments

Comments
 (0)