diff --git a/Dockerfile b/Dockerfile index 893990c..7a44a53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,10 +54,19 @@ COPY --from=builder /app/scripts ./scripts COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json +# Install Python and dependencies for tactical puzzles setup +# Users can run: docker exec chess-tutor python3 scripts/setup_tactical_puzzles.py +RUN apk add --no-cache python3 py3-pip zstd && \ + pip3 install --break-system-packages python-chess + # Copy entrypoint script and make executable COPY scripts/docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh +# Create directories for user-generated data +RUN mkdir -p /app/fixtures/tactics /app/downloads && \ + chown -R nextjs:nodejs /app/fixtures /app/downloads + # Set ownership to non-root user RUN chown -R nextjs:nodejs /app diff --git a/docker-compose.yml b/docker-compose.yml index 058bd5c..28e57cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,9 @@ services: volumes: # Persist Wikipedia cache to avoid re-fetching on container restart - wikipedia-cache:/app/public/wikipedia + # Persist tactical puzzles and downloads (user-generated data) + - tactical-fixtures:/app/fixtures + - puzzle-downloads:/app/downloads healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3050', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] @@ -42,3 +45,7 @@ networks: volumes: wikipedia-cache: driver: local + tactical-fixtures: + driver: local + puzzle-downloads: + driver: local diff --git a/src/app/imprint/page.tsx b/src/app/imprint/page.tsx index feffdb4..61781e1 100644 --- a/src/app/imprint/page.tsx +++ b/src/app/imprint/page.tsx @@ -5,7 +5,7 @@ import Footer from "@/components/Footer"; // Force dynamic rendering to read environment variables at runtime export const dynamic = 'force-dynamic'; -export default function ImprintPage() { +export default async function ImprintPage() { // If external imprint URL is configured, redirect to it const imprintUrl = process.env.IMPRINT_URL; diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index f427aa3..3977e58 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -16,6 +16,7 @@ export default function OnboardingPage() { const [apiKey, setApiKey] = useState(""); const [mounted, setMounted] = useState(false); const [error, setError] = useState(""); + const [consentGiven, setConsentGiven] = useState(false); useEffect(() => { const storedKey = localStorage.getItem("gemini_api_key"); @@ -63,6 +64,11 @@ export default function OnboardingPage() { return; } + if (!consentGiven) { + setError(t.onboarding.api.consentRequired); + return; + } + localStorage.setItem("gemini_api_key", trimmed); localStorage.setItem("chess_tutor_language", language); router.push("/"); @@ -205,7 +211,22 @@ export default function OnboardingPage() { placeholder={t.onboarding.api.placeholder} className="w-full p-3 rounded-lg border dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none" /> - {error &&
{error}
} + + {/* Consent Checkbox */} +{error}
}There is no such thing as a free lunch – LLM usage requires your own key.
{t.onboarding.api.storage}
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 0a8a688..a660087 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -14,6 +14,8 @@ export default function SettingsPage() { const [chesscomUsername, setChesscomUsername] = useState(""); const [lichessUsername, setLichessUsername] = useState(""); const [mounted, setMounted] = useState(false); + const [consentGiven, setConsentGiven] = useState(false); + const [showConsentError, setShowConsentError] = useState(false); // Load settings on mount useEffect(() => { @@ -22,7 +24,11 @@ export default function SettingsPage() { const storedChesscomUsername = localStorage.getItem("chesscom_username"); const storedLichessUsername = localStorage.getItem("lichess_username"); - if (storedKey) setApiKey(storedKey); + if (storedKey) { + setApiKey(storedKey); + // If there's already a stored key, consent was previously given + setConsentGiven(true); + } if (storedLang) setLanguage(storedLang as SupportedLanguage); if (storedChesscomUsername) setChesscomUsername(storedChesscomUsername); if (storedLichessUsername) setLichessUsername(storedLichessUsername); @@ -33,6 +39,14 @@ export default function SettingsPage() { const t = useTranslation(language); const handleSave = () => { + // Check consent if API key is being set + if (apiKey.trim() && !consentGiven) { + setShowConsentError(true); + return; + } + + setShowConsentError(false); + if (apiKey.trim()) { localStorage.setItem("gemini_api_key", apiKey.trim()); } else { @@ -115,7 +129,7 @@ export default function SettingsPage() { -+ {t.onboarding.api.consentRequired} +
+ )} +{t.start.apiKeyRequired} {t.start.getApiKey}
diff --git a/src/components/APIKeyInput.tsx b/src/components/APIKeyInput.tsx index 474d459..8161d3f 100644 --- a/src/components/APIKeyInput.tsx +++ b/src/components/APIKeyInput.tsx @@ -10,6 +10,8 @@ interface APIKeyInputProps { export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) { const [key, setKey] = useState(""); const [isOpen, setIsOpen] = useState(false); + const [consentGiven, setConsentGiven] = useState(false); + const [error, setError] = useState(""); useEffect(() => { const envKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY; @@ -26,11 +28,21 @@ export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (key.trim()) { - localStorage.setItem("gemini_api_key", key.trim()); - onKeySubmit(key.trim()); - setIsOpen(false); + + if (!key.trim()) { + setError("API key is required"); + return; } + + if (!consentGiven) { + setError("You must agree to store the API key in local storage to continue"); + return; + } + + localStorage.setItem("gemini_api_key", key.trim()); + onKeySubmit(key.trim()); + setIsOpen(false); + setError(""); }; if (!isOpen) { @@ -62,6 +74,27 @@ export function APIKeyInput({ onKeySubmit }: APIKeyInputProps) { className="w-full p-2 border rounded dark:bg-gray-700 dark:border-gray-600" required /> + + {/* Consent Checkbox */} ++ {error} +
+ )} +