Electronics Catalog: API, MCP server, frontend and deployment

Verified catalogue of mobiles and laptops sold in India, collected from
real retail listings (FastAPI backend, React frontend, Postgres/pgvector).

- REST API under /api/elec (read-only catalogue; admin endpoints need login)
- MCP server (FastMCP) at /mcp/ with list_categories, search_products,
  get_product and price_history tools
- Real ratings and reviews read from product pages and search results
- Production Dockerfile (requirements-api.txt, no PyTorch) and
  .env.production.example; remote database only via an explicit
  ELEC_ALLOW_REMOTE_DB host/name allowlist
- docs/API.md: endpoint and MCP reference with live examples

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
sriram
2026-10-01 12:17:42 +05:30
commit c7e4d59188
115 changed files with 14329 additions and 0 deletions

98
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,98 @@
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { useAuth } from './context/useAuth';
import { ErrorBoundary } from './components/ErrorBoundary';
import { LoginPage } from './pages/LoginPage';
/*
* Every page except the login screen is loaded on demand.
*
* Imported eagerly, the whole app landed in one ~790kB bundle, and every
* visitor downloaded all of it before they could reach the login form.
* Splitting per route means each page's code arrives when it is first opened.
* (The charting library that dominated that bundle went away with the
* analytics pages, but the split is still worth keeping.)
*
* 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 AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage })));
function PageFallback({ label }) {
return (
<div className="min-h-screen w-full bg-slate-950 text-slate-400 flex items-center justify-center text-sm">
{label}
</div>
);
}
function ProtectedRoute({ children, allowedRoles }) {
const { user, role, restoring } = useAuth();
// A token restored from sessionStorage is still being checked against
// /api/auth/me. Redirecting to /login now would bounce a valid session out
// on every refresh; rendering now would flash a dashboard about to be torn
// down if the token turns out to be expired.
if (restoring) {
return <PageFallback label="Restoring session..." />;
}
if (!user) {
return <Navigate to="/login" replace />;
}
const effectiveRole = role || (user.role === 'store' ? 'user' : user.role);
if (allowedRoles && !allowedRoles.includes(effectiveRole)) {
// Every remaining route is admin-only, so there is nowhere else in the app
// to send a non-admin. This can still be reached by a token minted before
// the `user` account was disabled - those stay valid for their 12h TTL -
// so it has to terminate somewhere public rather than redirect in a loop.
return <Navigate to="/login" replace />;
}
return children;
}
export default function App() {
return (
<ErrorBoundary>
<AuthProvider>
<BrowserRouter>
{/* Covers the network fetch for a lazily-loaded page chunk. */}
<Suspense fallback={<PageFallback label="Loading..." />}>
<Routes>
{/* Public Login Route */}
<Route path="/login" element={<LoginPage />} />
{/* Catalog Route - RESTRICTED TO ADMIN ONLY */}
<Route
path="/"
element={
<ProtectedRoute allowedRoles={['admin']}>
<HomePage />
</ProtectedRoute>
}
/>
{/* Admin Panel Route - RESTRICTED TO ADMIN ONLY */}
<Route
path="/admin"
element={
<ProtectedRoute allowedRoles={['admin']}>
<AdminPage />
</ProtectedRoute>
}
/>
{/* Catch-all redirect */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</BrowserRouter>
</AuthProvider>
</ErrorBoundary>
);
}