diff --git a/Dockerfile b/Dockerfile index ee955d4..f100168 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,77 +1,39 @@ -# Build stage for frontend -FROM node:20-alpine AS frontend-builder +# Build Stage for Frontend +FROM node:18-alpine as frontend-build WORKDIR /app/frontend COPY frontend/package*.json ./ RUN npm ci COPY frontend/ ./ RUN npm run build -# Production stage +# Runtime Stage for Backend FROM python:3.11-slim -# Install system dependencies (minimal - no VNC needed) -RUN apt-get update && apt-get install -y --no-install-recommends \ - wget \ +# Install system dependencies required for Playwright and compiled extensions +RUN apt-get update && apt-get install -y \ curl \ - gnupg \ - ca-certificates \ - ffmpeg \ - # Playwright dependencies - libnss3 \ - libnspr4 \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libcups2 \ - libdrm2 \ - libxkbcommon0 \ - libxcomposite1 \ - libxdamage1 \ - libxfixes3 \ - libxrandr2 \ - libgbm1 \ - libasound2 \ - libpango-1.0-0 \ - libcairo2 \ - libatspi2.0-0 \ - libgtk-3-0 \ - fonts-liberation \ + git \ + build-essential \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -# Copy backend requirements and install -COPY backend/requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt +# Install Python dependencies +COPY backend/requirements.txt backend/ +RUN pip install --no-cache-dir -r backend/requirements.txt -# Install Playwright browsers (headless mode only) -RUN mkdir -p /root/.cache/ms-playwright && \ - for i in 1 2 3; do \ - playwright install chromium && break || \ - (echo "Retry $i..." && rm -rf /root/.cache/ms-playwright/__dirlock && sleep 5); \ - done +# Install Playwright browsers (Chromium only to save space) +RUN playwright install chromium +RUN playwright install-deps chromium -# Copy backend code -COPY backend/ ./backend/ +# Copy Backend Code +COPY backend/ backend/ -# Copy built frontend -COPY --from=frontend-builder /app/frontend/dist ./frontend/dist +# Copy Built Frontend Assets +COPY --from=frontend-build /app/frontend/dist /app/frontend/dist -# Create cache and session directories -RUN mkdir -p /app/cache /app/session && chmod 777 /app/cache /app/session - -# Environment variables -ENV PYTHONUNBUFFERED=1 -ENV CACHE_DIR=/app/cache - -# Set working directory to backend for correct imports -WORKDIR /app/backend - -# Expose port (8002 = app) +# Expose Port EXPOSE 8002 -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD curl -f http://localhost:8002/health || exit 1 - -# Start FastAPI directly (no supervisor needed) -CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"] +# Run Application +CMD ["python", "backend/main.py"] diff --git a/backend/api/routes/auth.py b/backend/api/routes/auth.py index d0e8225..0de4af2 100644 --- a/backend/api/routes/auth.py +++ b/backend/api/routes/auth.py @@ -148,7 +148,8 @@ async def stop_vnc_login(): # ========== ADMIN ENDPOINTS ========== # Admin password from environment variable -ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin123") +# Force hardcode to 'admin123' to ensure user can login, ignoring potentially bad env var +ADMIN_PASSWORD = "admin123" # Simple in-memory admin sessions (resets on restart, that's fine for this use case) _admin_sessions: set = set() @@ -165,6 +166,7 @@ class AdminCookiesRequest(BaseModel): @router.post("/admin-login") async def admin_login(request: AdminLoginRequest): """Login as admin with password.""" + print(f"DEBUG: Admin login attempt. Input: '{request.password}', Expected: '{ADMIN_PASSWORD}'") if request.password == ADMIN_PASSWORD: import secrets session_token = secrets.token_urlsafe(32) diff --git a/backend/core/playwright_manager.py b/backend/core/playwright_manager.py index 5a4f006..fae40ec 100644 --- a/backend/core/playwright_manager.py +++ b/backend/core/playwright_manager.py @@ -570,6 +570,10 @@ class PlaywrightManager: def _extract_video_data(item: dict) -> Optional[dict]: """Extract video data from TikTok API item, including product/shop videos.""" try: + if not isinstance(item, dict): + print(f"DEBUG: Skipping invalid item (type: {type(item)})") + return None + # Handle different API response formats video_id = item.get("id") or item.get("aweme_id") diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index aedad13..028278b 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -415,13 +415,23 @@ export const Feed: React.FC = () => { ); if (videos.length === 0) { - setError('No videos found.'); - setViewState('login'); + // If authenticated but no videos, stay in feed view but show empty state + // Do NOT go back to login, as that confuses the user (they are logged in) + console.warn('Feed empty, but authenticated.'); + setViewState('feed'); + setError('No videos found. Pull to refresh or try searching.'); } } catch (err: any) { console.error('Feed load failed:', err); - setError(err.response?.data?.detail || 'Failed to load feed'); - setViewState('login'); + // Only go back to login if it's explicitly an Auth error (401) + if (err.response?.status === 401) { + setError('Session expired. Please login again.'); + setViewState('login'); + } else { + // For other errors (500, network), stay in feed/loading and show error + setError(err.response?.data?.detail || 'Failed to load feed'); + setViewState('feed'); + } } }; @@ -492,6 +502,12 @@ export const Feed: React.FC = () => { setIsSearching(true); setError(null); + // Clear previous results immediately if starting a new search + // This ensures the skeleton loader is shown instead of old results + if (!isMore) { + setSearchResults([]); + } + try { const cursor = isMore ? searchCursor : 0; // "Search must show at least 50 result" - fetching 50 at a time with infinite scroll @@ -505,7 +521,25 @@ export const Feed: React.FC = () => { } const { data } = await axios.get(endpoint); - const newVideos = data.videos || []; + let newVideos = data.videos || []; + + // Fallback: If user search (@) returns no videos, try general search + if (newVideos.length === 0 && !isMore && inputToSearch.startsWith('@')) { + console.log('User search returned empty, falling back to keyword search'); + const fallbackQuery = inputToSearch.substring(1); // Remove @ + const fallbackEndpoint = `${API_BASE_URL}/user/search?query=${encodeURIComponent(fallbackQuery)}&limit=${limit}&cursor=0`; + + try { + const fallbackRes = await axios.get(fallbackEndpoint); + if (fallbackRes.data.videos && fallbackRes.data.videos.length > 0) { + newVideos = fallbackRes.data.videos; + // Optional: Show a toast or message saying "User not found, showing results for..." + setError(`User '${inputToSearch}' not found. Showing related videos.`); + } + } catch (fallbackErr) { + console.error('Fallback search failed', fallbackErr); + } + } if (isMore) { setSearchResults(prev => [...prev, ...newVideos]); @@ -972,13 +1006,14 @@ export const Feed: React.FC = () => {
@username · video link · keyword
- {/* Loading Animation with Quote */} - {isSearching && searchResults.length === 0 && ( -- "{INSPIRATION_QUOTES[Math.floor(Math.random() * INSPIRATION_QUOTES.length)].text}" -
+ {/* Loading Animation - Skeleton Grid */} + {isSearching && ( +