diff --git a/README.md b/README.md index 3c8382d..e055b66 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,16 @@ is a fast local reference. ## Quick start -### One-click (Windows) +### One command (from the project root) -Double-click **`start_frontend.bat`**. It checks whether the backend is -reachable on port 8000 (just a warning, not a blocker) and starts the -Vite dev server. +```bash +python run_project.py +``` + +Starts the FastAPI backend, waits until `/api/health` answers, then starts +this Vite dev server - so the API is always up before the UI first calls it. +Use `--frontend-only` to start just Vite against a backend you're already +running yourself. ### Manual @@ -25,8 +30,11 @@ npm run dev Open http://localhost:5173 - the Vite dev server proxies `/api/*` to `http://localhost:8000` automatically (see `vite.config.js`), so make -sure the backend is running there first (`../backend/README.md`, -`../backend/start_backend.bat`). +sure the backend is running there first (see `../backend/README.md`). + +Note that Vite binds `localhost` as IPv6 (`[::1]`), which browsers handle +fine. Only relevant if you probe it with a tool pinned to IPv4 - use +`http://[::1]:5173` there, or pass `--host 127.0.0.1`. If your backend runs somewhere else, set `VITE_API_BASE_URL` in a local `.env` file (copy `.env.example`) instead of relying on the proxy. diff --git a/nginx.conf b/nginx.conf index 0b43a0c..98f33ad 100644 --- a/nginx.conf +++ b/nginx.conf @@ -37,6 +37,24 @@ http { proxy_read_timeout 300s; } + # FastAPI's interactive docs. These are served at the app root by + # FastAPI (/docs, /redoc, /openapi.json) rather than under /api, so + # without this block they fall through to the SPA below and return + # index.html instead of Swagger. + # + # This publishes your full API surface to anyone who finds the URL. + # To close it off, delete this block and reach the docs through an SSH + # tunnel instead: ssh -L 8000:localhost:8000 user@vps + location ~ ^/(docs|redoc|openapi\.json) { + set $docs_upstream "http://backend:8000"; + proxy_pass $docs_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/src/App.jsx b/src/App.jsx index 6601a3f..c0091fb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,17 +1,50 @@ -import React from 'react'; +import React, { Suspense, lazy } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider, useAuth } from './context/AuthContext'; +import { AuthProvider } from './context/AuthContext'; +import { useAuth } from './context/useAuth'; import { ErrorBoundary } from './components/ErrorBoundary'; import { LoginPage } from './pages/LoginPage'; -import { HomePage } from './pages/HomePage'; -import { UserPage } from './pages/UserPage'; -import { AdminPage } from './pages/AdminPage'; -import { StoresPage } from './pages/StoresPage'; -import { AnalyticsPage } from './pages/AnalyticsPage'; -import { NutritionAnalyticsPage } from './pages/NutritionAnalyticsPage'; + +/* + * Every page except the login screen is loaded on demand. + * + * Imported eagerly, the whole app landed in one ~790kB bundle - the charting + * library alone, pulled in by the two analytics pages, was most of it, and + * every visitor downloaded it before they could reach the login form. Splitting + * per route means each page's code arrives when it is first opened. + * + * LoginPage stays eager: it is the first thing an unauthenticated visitor + * sees, so deferring it would only add a round trip before the form appears. + */ +const HomePage = lazy(() => import('./pages/HomePage').then((m) => ({ default: m.HomePage }))); +const UserPage = lazy(() => import('./pages/UserPage').then((m) => ({ default: m.UserPage }))); +const AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage }))); +const StoresPage = lazy(() => import('./pages/StoresPage').then((m) => ({ default: m.StoresPage }))); +const AnalyticsPage = lazy(() => + import('./pages/AnalyticsPage').then((m) => ({ default: m.AnalyticsPage })) +); +const NutritionAnalyticsPage = lazy(() => + import('./pages/NutritionAnalyticsPage').then((m) => ({ default: m.NutritionAnalyticsPage })) +); + +function PageFallback({ label }) { + return ( +
+ {submitError} +
+ )} + {runningJob && (A job for "{runningJob.brand}" is already in progress - please wait. diff --git a/src/pages/AnalyticsPage.jsx b/src/pages/AnalyticsPage.jsx index 41f556d..1b5d65f 100644 --- a/src/pages/AnalyticsPage.jsx +++ b/src/pages/AnalyticsPage.jsx @@ -1,5 +1,4 @@ import { useEffect, useState, useCallback } from 'react'; -import { Link } from 'react-router-dom'; import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index aaa1700..e9caef3 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; import { ShieldCheck, UserCheck, Lock, User, ArrowRight, Sparkles } from 'lucide-react'; export function LoginPage() { @@ -29,13 +29,17 @@ export function LoginPage() { setLoading(true); try { - const user = await login(username, password, selectedRole); + const user = await login(username, password); setLoading(false); + // Route by the role the SERVER assigned, not by the tab that was + // selected. The tab only labels which account you are signing in to. if (user.role === 'admin') navigate('/admin'); else navigate('/user'); } catch (err) { setLoading(false); - setError(`Login failed. Invalid username or password for ${selectedRole.toUpperCase()} account.`); + // Surface the server's own message: it distinguishes bad credentials + // from the lockout after repeated failures, which a generic string hides. + setError(err?.message || 'Login failed. Check your username and password.'); } }; diff --git a/src/pages/NutritionAnalyticsPage.jsx b/src/pages/NutritionAnalyticsPage.jsx index 5ddc2a0..aa7ab4e 100644 --- a/src/pages/NutritionAnalyticsPage.jsx +++ b/src/pages/NutritionAnalyticsPage.jsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from 'react'; -import { Link } from 'react-router-dom'; import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, diff --git a/src/pages/StoresPage.jsx b/src/pages/StoresPage.jsx index dfc1bf5..713f1c3 100644 --- a/src/pages/StoresPage.jsx +++ b/src/pages/StoresPage.jsx @@ -1,5 +1,4 @@ import { useEffect, useState, useCallback } from 'react'; -import { Link } from 'react-router-dom'; import { Store as StoreIcon, TrendingUp, IndianRupee, PackageCheck, PackageX, PackageSearch, BarChart3, Search, UploadCloud, diff --git a/src/pages/UserPage.jsx b/src/pages/UserPage.jsx index d8d3d74..8ef990c 100644 --- a/src/pages/UserPage.jsx +++ b/src/pages/UserPage.jsx @@ -5,9 +5,9 @@ import { ProductCard } from '../components/ProductCard'; import { ProductModal } from '../components/ProductModal'; import { ErrorBoundary } from '../components/ErrorBoundary'; import { - PlusCircle, Upload, CheckCircle2, Database, FileText, Sparkles, - RefreshCw, Search, FileSpreadsheet, Download, AlertCircle, ShoppingBag, - Store, LayoutGrid, Filter, Tag, ArrowRight + PlusCircle, Upload, CheckCircle2, Database, Sparkles, + RefreshCw, Search, FileSpreadsheet, Download, ShoppingBag, + Store, LayoutGrid, Filter, ArrowRight } from 'lucide-react'; const STORES_SAMPLE = [