# Project export: FluxCareAI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: Do you wonder if the consumer products you're consuming are safe? Find out with our app!
- Devpost: https://devpost.com/software/consumerhealth
- GitHub: https://github.com/rubenyh/fusion-cruzhacks2.0
- Video: https://www.youtube.com/embed/kAjsQtYulf8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — rubenyh (8 commits), Khoa Nguyen (7 commits)

## Devpost submission (written by the team)

### Inspiration

Consumers are expected to make healthy and safe choices, yet the information needed to do so is often scattered, technical, or hidden in regulatory databases, lawsuits, and long reports. Most people don’t have the time or expertise to interpret FDA violations, recalls, or media investigations before buying or consuming a product. We wanted to make product transparency instant and accessible, allowing anyone to quickly understand whether a product is safe, trustworthy, and healthy.

### What it does

FluxCareAI lets users take a photo of a consumer product and receive a clear, evidence-backed safety and trust report about the product and the company behind it. The app identifies the product, resolves the correct manufacturer, and analyzes lawsuits, FDA warnings, recalls, and documented safety concerns. Results are presented in a client-friendly format explaining what was found, what the risks are, and gives recommendations to the client.

### How we built it

We built FluxCareAI using Expo with React Native for the mobile app and FastAPI for the backend. The frontend handles image capture and report display, while the backend coordinates a multi-agent AI pipeline. After a photo is submitted, AI agents identify the product, resolve the correct company, gather evidence from the FDA, lawsuits, and news sources, and synthesize the findings into a client-friendly report.

### Challenges we ran into

A major challenge was defining the right context and scope for each AI agent. Overly broad research instructions caused agents to take on too much work, which could disrupt the report-generation process. We solved this by clearly constraining each agent’s role, improving reliability and performance.

### Accomplishments we're proud of

We are proud to build a fully functional end-to-end system that turns a simple product photo into a detailed, evidence-backed safety report. We’re especially proud of creating a multi-agent AI pipeline that reliably gathers regulatory and legal information while enforcing credibility and transparency before reaching the user.

### What we learned

We learned the importance of clear task definition and scoped responsibilities when working with multiple AI agents. We also gained experience translating complex regulatory and legal data into information that is clear, accurate, and usable for everyday consumers.

### What's next

With sufficient funding, we plan to evolve FluxCareAI into a fully released product or startup. Next steps include scaling the infrastructure, expanding coverage across more product categories, improving real-time data access, and refining the user experience to make FluxCare a trusted consumer safety tool.

## README (from the GitHub repository)

# Image Upload Feature

This feature allows users to upload images or take photos directly from their mobile device and send them to a backend API.

## Frontend Implementation

### Features
- **Camera Access**: Take photos directly from the camera
- **Gallery Access**: Select images from the device's photo library
- **Image Preview**: Shows selected image before upload
- **Upload Progress**: Loading indicator during upload
- **Error Handling**: User-friendly error messages
- **Permissions**: Automatic permission requests for camera and media library

### API Configuration

The API endpoint is configured via environment variable in `frontend/.env`:

```env
EXPO_PUBLIC_API_BASE_URL=http://192.168.1.42:8000
```

The upload endpoint is `/api/upload`.

### Usage
1. Tap "Upload / Take Picture" button
2. Choose between "Take Photo" or "Choose from Gallery"
3. Grant necessary permissions if prompted
4. Preview the selected image
5. Tap "Upload Image" to send to backend
6. Success/error message will be displayed

## Backend Implementation

### FastAPI Server
The backend uses FastAPI with automatic OpenAPI documentation.

### Running the FastAPI Server

1. **Install dependencies**:
   ```bash
   cd backend
   pip install -r requirements.txt
   ```

2. **Start the server**:
   ```bash
   # Using the startup script
   ./start-server.sh

   # Or manually
   uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
   ```

3. **Access points**:
   - API: `http://localhost:8000`
   - Documentation: `http://localhost:8000/docs`
   - Health check: `http://localhost:8000/api/health`

### API Requirements

**Endpoint**: `POST /api/upload`

**Content-Type**: `multipart/form-data`

**Form Field**: `image` (file upload)

**Response Format**:
```json
{
  "success": true,
  "message": "Image uploaded successfully",
  "image": {
    "filename": "image-123456789.jpg",
    "originalName": "photo.jpg",
    "size": 1024000,
    "mimetype": "image/jpeg",
    "path": "uploads/image-123456789.jpg",
    "uploadedAt": "2024-01-17T12:00:00.000Z"
  },
  "analysis": {
    // Your AI/ML analysis results here
  }
}
```

### Error Responses
```json
{
  "error": "Error message",
  "details": "Additional error details"
}
```

## Setup Instructions

### Frontend
1. Packages are already installed (`expo-image-picker`, `expo-media-library`)
2. Environment variable is configured in `.env`
3. Update `EXPO_PUBLIC_API_BASE_URL` to match your server IP/port

### Backend
1. Install Python dependencies: `pip install -r requirements.txt`
2. Create `uploads/` directory in backend folder
3. Run the server: `uvicorn app.main:app --reload --host 0.0.0.0 --port 8000`
4. Update frontend `.env` with correct server URL

## Security Considerations
- Implement authentication/authorization
- Validate file types and sizes
- Consider rate limiting
- Store uploaded files securely
- Add image processing/validation

## Customization
- Modify image quality/compression in `ImagePicker.launchCameraAsync()` and `launchImageLibraryAsync()`
- Change upload limits in the backend
- Add image resizing/processing
- Implement authentication headers
- Add progress indicators for large uploads

## Auth0 Authentication Setup

The app includes Auth0 authentication integration. When users are not signed in, the history section shows "Login to save history!" with a login button.

### Auth0 Setup Steps

1. **Create Auth0 Account & Application**:
   - Go to [auth0.com](https://auth0.com) and create an account
   - Create a new Application (Native App)
   - Note down your Domain and Client ID

2. **Configure Allowed Callback URLs**:
   - In Auth0 Dashboard → Applications → Your App → Settings
   - Add these URLs to "Allowed Callback URLs":
     ```
     http://localhost:8081,http://localhost:8082,exp://localhost:8081,exp://localhost:8082
     ```
   - Add to "Allowed Logout URLs":
     ```
     http://localhost:8081,http://localhost:8082,exp://localhost:8081,exp://localhost:8082
     ```

3. **Update Environment Variables**:
   - Edit `frontend/.env`:
     ```env
     EXPO_PUBLIC_AUTH0_DOMAIN=your-domain.auth0.com
     EXPO_PUBLIC_AUTH0_CLIENT_ID=your-client-id
     EXPO_PUBLIC_AUTH0_AUDIENCE=https://your-api-identifier
     ```

4. **Update App Configuration**:
   - Edit `frontend/app.json` in the react-native-auth0 plugin section:
     ```json
     [
       "react-native-auth0",
       {
         "domain": "your-domain.auth0.com",
         "clientId": "your-client-id"
       }
     ]
     ```

### Authentication Features

- **Login/Logout**: Available through the drawer menu
- **User Info**: Shows user name and email in drawer when authenticated
- **Conditional UI**: History section changes based on auth state
- **Token Management**: Automatic token storage and refresh

### Testing Authentication

1. Start the app: `npm start`
2. Open drawer (☰ button)
3. Tap "Log In / Sign Up"
4. Complete Auth0 authentication flow
5. User info should appear in drawer
6. History section should show "No history yet..." instead of login prompt

## Detected evidence (automated analysis)

Indexed codebase: 35 recognized source files, 101 KB.
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- AWS (technology) — claimed on Devpost, not found in the code
- MongoDB (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (44 of 44)

```
.gitignore
backend/app/agent_function.py
backend/app/agents.py
backend/app/auth.py
backend/app/db.py
backend/app/DetectService.py
backend/app/json_to_pdf.py
backend/app/main.py
backend/app/private_keys.json
backend/app/routes.py
backend/app/schemas.py
backend/example-server.js
backend/requirements.txt
frontend/.gitignore
frontend/.vscode/extensions.json
frontend/.vscode/settings.json
frontend/app.json
frontend/app/_layout.tsx
frontend/app/(drawer)/_layout.tsx
frontend/app/(drawer)/(tabs)/_layout.tsx
frontend/app/(drawer)/(tabs)/stackhome/_layout.tsx
frontend/app/(drawer)/(tabs)/stackhome/index.tsx
frontend/app/(drawer)/(tabs)/stackhome/results.tsx
frontend/app/modal.tsx
frontend/components/external-link.tsx
frontend/components/haptic-tab.tsx
frontend/components/hello-wave.tsx
frontend/components/parallax-scroll-view.tsx
frontend/components/themed-text.tsx
frontend/components/themed-view.tsx
frontend/components/ui/collapsible.tsx
frontend/components/ui/icon-symbol.ios.tsx
frontend/components/ui/icon-symbol.tsx
frontend/constants/theme.ts
frontend/context/AuthContext.tsx
frontend/eslint.config.js
frontend/hooks/use-color-scheme.ts
frontend/hooks/use-color-scheme.web.ts
frontend/hooks/use-theme-color.ts
frontend/package.json
frontend/README.md
frontend/scripts/reset-project.js
frontend/tsconfig.json
README.md
```

### Dependencies

- backend/requirements.txt: boto3, fastapi, google-generativeai, groq, langchain-groq, pymongo[srv]@>=4.9,<5.0, python-dotenv, python-jose[cryptography], reportlab, uagents, uvicorn
- frontend/package.json: @expo/vector-icons@^15.0.3, @react-navigation/bottom-tabs@^7.4.0, @react-navigation/drawer@^7.7.12, @react-navigation/elements@^2.6.3, @react-navigation/native@^7.1.8, @types/react@~19.1.0, base-64@^1.0.0, eslint@^9.25.0, eslint-config-expo@~10.0.0, expo@~54.0.31, expo-auth-session@~5.5.2, expo-constants@~18.0.13, expo-font@~14.0.10, expo-haptics@~15.0.8, expo-image@~3.0.11, expo-image-picker@~17.0.10, expo-linking@~8.0.11, expo-media-library@~18.2.1, expo-router@~6.0.21, expo-secure-store@^15.0.8, expo-sharing@^14.0.8, expo-splash-screen@~31.0.13, expo-status-bar@~3.0.9, expo-symbols@~1.0.8, expo-system-ui@~6.0.9, expo-web-browser@~15.0.10, react@19.1.0, react-dom@19.1.0, react-native@0.81.5, react-native-gesture-handler@~2.28.0, react-native-reanimated@~4.1.1, react-native-safe-area-context@~5.6.0, react-native-screens@~4.16.0, react-native-web@~0.21.0, react-native-worklets@0.5.1, typescript@~5.9.2

### Recent commits (newest first)

- history and pdf fully working
- finished db history, wip: ui history
- add_db and auth
- edit endpoints, and new page
- some changes
- Merge pull request #1 from rubenyh/backend
- Merge branch 'main' into backend
- very awesome
- test
- add json to pdf
- poo
- auth0 login/signup
- fixed camera upload
- updated frontend, upload/take picture
- initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### backend/requirements.txt

```
# MongoDB async driver
pymongo[srv]>=4.9,<5.0
# JWT auth
python-jose[cryptography]
fastapi
uvicorn
python-dotenv
langchain-groq
python-dotenv
google-generativeai
groq
uagents
reportlab
boto3
```

### frontend/package.json

```
{
  "name": "frontend",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^15.0.3",
    "@react-navigation/bottom-tabs": "^7.4.0",
    "@react-navigation/drawer": "^7.7.12",
    "@react-navigation/elements": "^2.6.3",
    "@react-navigation/native": "^7.1.8",
    "base-64": "^1.0.0",
    "expo": "~54.0.31",
    "expo-auth-session": "~5.5.2",
    "expo-constants": "~18.0.13",
    "expo-font": "~14.0.10",
    "expo-haptics": "~15.0.8",
    "expo-image": "~3.0.11",
    "expo-image-picker": "~17.0.10",
    "expo-linking": "~8.0.11",
    "expo-media-library": "~18.2.1",
    "expo-router": "~6.0.21",
    "expo-secure-store": "^15.0.8",
    "expo-sharing": "^14.0.8",
    "expo-splash-screen": "~31.0.13",
    "expo-status-bar": "~3.0.9",
    "expo-symbols": "~1.0.8",
    "expo-system-ui": "~6.0.9",
    "expo-web-browser": "~15.0.10",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.5",
    "react-native-gesture-handler": "~2.28.0",
    "react-native-reanimated": "~4.1.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.5.1"
  },
  "devDependencies": {
    "@types/react": "~19.1.0",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~10.0.0",
    "typescript": "~5.9.2"
  },
  "private": true
}

```

### frontend/app/_layout.tsx

```typescript
import React from 'react';
import { ThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { AuthProvider } from '@/context/AuthContext';
import { useColorScheme } from '@/hooks/use-color-scheme';

export const unstable_settings = { anchor: '(drawer)' };

export default function RootLayout() {
  const colorScheme = useColorScheme();

  return (
    <AuthProvider>
      <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
        <Stack>
          <Stack.Screen name="(drawer)" options={{ headerShown: false }} />
          <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
        </Stack>
        <StatusBar style="auto" />
      </ThemeProvider>
    </AuthProvider>
  );
}

```

### backend/app/main.py

```python
import asyncio, uuid, os, json, base64
import httpx
from pydantic import BaseModel
from uagents_core.envelope import Envelope
from uagents_core.identity import Identity
from fastapi import FastAPI, Request, UploadFile, File, HTTPException, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from .DetectService import read_image_bytes, detect_ingredients
from .json_to_pdf import json_to_pdf
from .db import client
from datetime import datetime
from uagents_core.models import Model as UA_Model
from .agents import DetectionInput, detect_agent
import boto3
from io import BytesIO

# AWS Config
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET")
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")

s3_client = boto3.client(
    "s3",
    aws_access_key_id=AWS_ACCESS_KEY_ID,
    aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
    region_name=AWS_REGION
)

# FastAPI
app = FastAPI(title="Image -> Agents -> PDF")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"], allow_credentials=True,
    allow_methods=["*"], allow_headers=["*"]
)

RESULTS_DIR = "report_results"
os.makedirs(RESULTS_DIR, exist_ok=True)

_CLIENT_IDENTITY: Identity | None = None
try:
    keys_path = os.path.join(os.path.dirname(__file__), "private_keys.json")
    with open(keys_path, "r", encoding="utf8") as _kf:
        _keys = json.load(_kf)
        _test_key = _keys.get("test_client", {}).get("identity_key")
        if _test_key:
            _CLIENT_IDENTITY = Identity.from_string(_test_key)
except Exception:
    _CLIENT_IDENTITY = None

PENDING: dict[str, asyncio.Future] = {}
DETECT_AGENT_SUBMIT = os.getenv("DETECT_AGENT_SUBMIT", "http://127.0.0.1:8000/submit")
PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "http://127.0.0.1:8080")
DB_NAME = os.environ.get("MONGO_DB", "cruzhack")
COLL_NAME = os.environ.get("MONGO_COLLECTION", "reports")
coll = client[DB_NAME][COLL_NAME]

# Webhook endpoint
@app.post("/report/webhook/{request_id}")
async def report_webhook(request_id: str, request: Request):
    payload = await request.json()
    fut = PENDING.get(request_id)
    if fut and not fut.done():
        fut.set_result(payload)

    final_report = payload.get("final_report")
    if final_report:
        await asyncio.to_thread(
            coll.update_one,
            {"request_id": request_id},
            {"$set": {"final_report": final_report, "status": "complete", "completed_at": datetime.utcnow()}}
        )
    return {"ok": True}

# POST image -> generate report
@app.post("/report-json")
async def report_json(image: UploadFile = File(...), user=Depends(lambda: {"sub": "test_user"})):
    if not image.content_type or not image.content_type.startswith("image/"):
        raise HTTPException(400, "Upload must be an image")

    img_bytes = await image.read()
    await image.close()

    # Upload to S3 without ACL (avoid bucket errors)
    s3_key = f"images/{uuid.uuid4().hex}.{image.filename.split('.')[-1]}"
    await asyncio.to_thread(
        s3_client.upload_fileobj,
        BytesIO(img_bytes),
        AWS_S3_BUCKET,
        s3_key,
        ExtraArgs={"ContentType": image.content_type}
    )

    # Proper URL to avoid 301 redirect
    if AWS_REGION == "us-east-1":
        s3_url = f"https://{AWS_S3_BUCKET}.s3.amazonaws.com/{s3_key}"
    else:
        s3_url = f"https://{AWS_S3_BUCKET}.s3.{AWS_REGION}.amazonaws.com/{s3_key}"

    detection = detect_ingredients(img_bytes)
    original_detection = detection.model_dump() if hasattr(detection, "model_dump") else detection

    request_id = uuid.uuid4().hex
    callback_url = f"{PUBLIC_BASE_URL}/report/webhook/{request_id}"

    # Insert initial report to MongoDB
    doc = {
        "user_id": user.get("sub"),
        "request_id": request_id,
        "detection": original_detection,
        "image_url": s3_url,
        "status": "pending",
        "created_at": datetime.utcnow()
    }
    await asyncio.to_thread(coll.insert_one, doc)

    # Prepare agent envelope
    loop = asyncio.get_running_loop()
    fut = loop.create_future()
    PENDING[request_id] = fut

    try:
        digest = UA_Model.build_schema_digest(DetectionInput)
    except Exception:
        digest = "detectioninput-v1"

    message = DetectionInput(
        detection_result=original_detection,
        request_id=request_id,
        callback_url=callback_url,
    )

    payload_json = json.dumps(message.model_dump(), separators=(",", ":"), ensure_ascii=False)
    payload_b64 = base64.b64encode(payload_json.encode()).decode()

    env = Envelope(
        version=1,
        sender=_CLIENT_IDENTITY.address if _CLIENT_IDENTITY else "fastapi",
        target=detect_agent.address,
        session=uuid.uuid4(),
        schema_digest=digest,
        payload=payload_b64,
    )
    if _CLIENT_IDENTITY:
        env.sign(_CLIENT_IDENTITY)
    envelope = env.model_dump()
    if envelope.get("session") is not None:
        envelope["session"] = str(envelope["session"])

    async with httpx.AsyncClient(timeout=30.0) as client_http:
        r = await client_http.post(DETECT_AGENT_SUBMIT, json=envelope)
        if r.status_code >= 300:
            PENDING.pop(request_id, None)
            raise HTTPException(500, f"detect_agent submit failed: {r.status_code} {r.text[:200]}")

    try:
        payload = await asyncio.wait_for(fut, timeout=90.0)
    except asyncio.TimeoutError:
        PENDING.pop(request_id, None)
        raise HTTPException(504, "Timed out waiting for writer webhook")

    final_report = payload.get("final_report")
    if not isinstance(final_report, dict):
        raise HTTPException(500, "Webhook did not return final_report")

    # Update MongoDB with final report
    await asyncio.to_thread(
        coll.update_one,
        {"request_id": request_id},
        {"$set": {"final_report": final_report, "status": "complete", "completed_at": datetim
[truncated — 2500 more characters]
```

### frontend/app/(drawer)/_layout.tsx

```typescript
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { DrawerActions } from "@react-navigation/native";
import * as Haptics from 'expo-haptics';
import { useNavigation, useRouter } from "expo-router";
import { Drawer } from "expo-router/drawer";
import { useState } from "react";
import { Dimensions, Platform, Pressable, StyleSheet, Switch, Text, TouchableOpacity, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useAuth } from "@/context/AuthContext";
const width = Dimensions.get("window").width;

const customTitles: Record<string, string> = {
  contact: "Contacto",
  faq: width <= 410 ? "FAQ" : "Preguntas Frecuentes",
  about: "Sobre Nosotros",
  "settings/index": "Configuración",
};

export default function Layout() {
  const router = useRouter();

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        screenOptions={({ route }: { route: { name: string } }) => ({
          headerShown: Object.keys(customTitles).includes(route.name) || route.name === "actualizarDatos",
          title: customTitles[route.name] || (route.name === "actualizarDatos" ? "Actualizar Datos" : route.name),
          headerLeft: () => {
            if (route.name === "actualizarDatos") {
              return (
                <TouchableOpacity 
                  style={{ marginLeft: 10 }}
                  onPress={() => router.back()}
                >
                  <MaterialCommunityIcons name="arrow-left" size={24} color="#15193a" />
                </TouchableOpacity>
              );
            }
            return null; // Remove the drawer button from here since tabs handle it
          },
        })}
        drawerContent={() => <CustomDrawerContent />}
      />
    </GestureHandlerRootView>
  );
}

function CustomDrawerContent() {
  const router = useRouter();
  const { user, isAuthenticated, login, logout } = useAuth();
  const [isDarkMode, setIsDarkMode] = useState(false);

  const handleAuth = async () => {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    try {
      if (isAuthenticated) {
        await logout();
      } else {
        await login();
      }
    } catch (error) {
      console.error('Auth error:', error);
    }
  };

  const toggleTheme = () => {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    setIsDarkMode(previousState => !previousState);
  };

  const menuItems: { title: string; path: string }[] = [
    { title: "Home", path: '(tabs)/stackhome' },
  ];

  return (
    <View style={styles.drawerContainer}>
      <View style={styles.titleContainer}>
        <View style={styles.headerContainer}>
          <Text style={styles.titleText}>Menu</Text>
        </View>

        {isAuthenticated && user && (
          <View style={styles.userInfo}>
            <Text style={styles.userName}>{user.name || 'User'}</Text>
            <Text style={styles.userEmail}>{user.email}</Text>
          </View>
        )}

        <View>
          <View style={styles.divider} />
          {menuItems.map((item, index) => (
            <View key={index}>
              <TouchableOpacity
                style={styles.menuItemContainer}
                onPress={() => {
                  Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
                  router.navigate(item.path as any);
                }}
              >
                <Text style={styles.drawerItem}>{item.title}</Text>
              </TouchableOpacity>
              <View style={styles.dividerItems} />
            </View>
          ))}
        </View>
      </View>

      <Pressable
        onPress={handleAuth}
        style={({ pressed }) => [styles.logoutButton, pressed ? styles.logoutButtonPressed : {}]}
      >
        <MaterialCommunityIcons
          name={isAuthenticated ? 'logout' : 'login'}
          size={20}
          color="white"
        />
        <Text style={styles.logoutText}>
          {isAuthenticated ? 'Logout' : 'Log In / Sign Up'}
        </Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  drawerContainer: {
    flex: 1,
    padding: 24,
    justifyContent: "space-between",
    backgroundColor: "#333552",
  },
  titleContainer: {
    marginTop: 40,
  },
  headerContainer: {
    flexDirection: "row",
    alignItems: "center",
    marginBottom: 10,
  },
  titleText: {
    fontSize: 24,
    fontWeight: "bold",
    color: "white",
    marginLeft: 0,
  },
  divider: {
    height: 1,
    backgroundColor: "#CCCCCC",
    marginVertical: 15,
  },
  dividerItems: {
    height: 1,
    backgroundColor: "#CCCCCC",
    marginVertical: 15,
  },
  themeContainer: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    paddingVertical: 10,
  },
  themeTextContainer: {
    flexDirection: "row",
    alignItems: "center",
  },
  themeText: {
    fontSize: 16,
    fontWeight: "bold",
    color: "white",
    marginLeft: 15,
  },
  menuItemContainer: {
    flexDirection: "row",
    alignItems: "center",
    paddingVertical: 12,
    borderRadius: 8,
  },
  drawerItem: {
    fontSize: 16, 
    fontWeight: "bold", 
    color: "white", 
    marginLeft: 15,
  },
  logoutButton: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
    padding: 12,
    borderRadius: 8,
    backgroundColor: "#15193a",
    justifyContent: "center",
  },
  logoutButtonPressed: {
    backgroundColor: "#202552",
  },
  logoutText: {
    color: "white",
    fontSize: 16,
    fontWeight: "600",
  },
  userInfo: {
    paddingVertical: 16,
    paddingHorizontal: 8,
  },
  userName: {
    fontSize: 18,
    fontWeight: "bold",
    color: "white",
    marginBottom: 4,
  },
  userEmail: {
    fontSize: 14,
    color: "#cccccc",
  },
});
```

### frontend/app/(drawer)/(tabs)/_layout.tsx

```typescript
import { Tabs } from 'expo-router';
import React from 'react';
import { TouchableOpacity, View } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { DrawerActions } from '@react-navigation/native';
import { useNavigation } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';

import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';

const CustomDrawerButton = () => {
  const navigation = useNavigation();

  const openDrawer = () => {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    navigation.dispatch(DrawerActions.openDrawer());
  };

  return (
    <TouchableOpacity
      style={{ padding: 8, marginLeft: 8 }}
      onPress={openDrawer}
    >
      <MaterialCommunityIcons name="menu" size={24} color="#f9f9f9" />
    </TouchableOpacity>
  );
};

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
      tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
      headerShown: true,
      headerTransparent: true,
      headerLeft: () => <CustomDrawerButton />,
      headerStyle: {
        backgroundColor: '#15193a',
      },
      tabBarStyle: {
        backgroundColor: '#15193a',
      },
      tabBarButton: HapticTab,
      }}>
      <Tabs.Screen
      name="stackhome"  
      options={{
        title: '',
        tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
      }}
      />
    </Tabs>
  );
}

```

### frontend/app/(drawer)/(tabs)/stackhome/_layout.tsx

```typescript
import { Stack } from 'expo-router';
import React from 'react';

export default function StackLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: '' }} />
      <Stack.Screen name="results" options={{ title: 'Results' }} />
    </Stack>
  );
}

```

### frontend/app/(drawer)/(tabs)/stackhome/index.tsx

```typescript
import React, { useState, useEffect } from "react";
import { View, Text, TouchableOpacity, StyleSheet, Alert, Image, ActivityIndicator, ScrollView } from "react-native";
import * as ImagePicker from "expo-image-picker";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useAuth } from "@/context/AuthContext";
import { useRouter } from "expo-router";

const API_CONFIG = { 
  baseUrl: process.env.EXPO_PUBLIC_API_BASE_URL!, 
  uploadEndpoint: "/report-json",
  historyEndpoint: "/reports"
};

export default function UploadScreen() {
  const [selectedImage, setSelectedImage] = useState<string | null>(null);
  const [isUploading, setIsUploading] = useState(false);
  const [history, setHistory] = useState<any[]>([]);
  const [loadingHistory, setLoadingHistory] = useState(false);
  const { isAuthenticated, login } = useAuth();
  const router = useRouter();

  const requestPermissions = async () => {
    const camera = await ImagePicker.requestCameraPermissionsAsync();
    const media = await ImagePicker.requestMediaLibraryPermissionsAsync();
    if (camera.status !== "granted" || media.status !== "granted") {
      Alert.alert("Permissions Required", "Camera and media library permissions are required.");
      return false;
    }
    return true;
  };

  const takePhoto = async () => {
    if (!(await requestPermissions())) return;
    const result = await ImagePicker.launchCameraAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [4,3], quality: 0.8 });
    if (!result.canceled && result.assets[0]) setSelectedImage(result.assets[0].uri);
  };

  const pickFromGallery = async () => {
    if (!(await requestPermissions())) return;
    const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [4,3], quality: 0.8 });
    if (!result.canceled && result.assets[0]) setSelectedImage(result.assets[0].uri);
  };

const uploadImage = async () => {
  if (!selectedImage) {
    Alert.alert("No Image", "Please select or take a photo first.");
    return;
  }

  setIsUploading(true);

  try {
    const formData = new FormData();

    formData.append("image", {
      uri: selectedImage,
      name: "photo.jpg",
      type: "image/jpeg",
    } as any);

    const res = await fetch(
      `${API_CONFIG.baseUrl}${API_CONFIG.uploadEndpoint}`,
      {
        method: "POST",
        body: formData,
      }
    );

    if (!res.ok) throw new Error(`Upload failed: ${res.status}`);

    const json = await res.json();

router.push({
  pathname: "/(drawer)/(tabs)/stackhome/results",
  params: {
    report: JSON.stringify({
      ...json.final_report,
      request_id: json.request_id,
      image_url: json.image_url,
    }),
  },
});

    setSelectedImage(null);
    fetchHistory();
  } catch (err: any) {
    Alert.alert("Upload Failed", err.message || String(err));
  } finally {
    setIsUploading(false);
  }
};

const fetchHistory = async () => {
  if (!isAuthenticated) return;

  setLoadingHistory(true);

  try {
    const res = await fetch(`${API_CONFIG.baseUrl}${API_CONFIG.historyEndpoint}`);
    const data = await res.json();

    console.log("Fetched history:", data);

    // Backend returns an array directly now
    const reports = Array.isArray(data) ? data : [];

    setHistory(
      reports.sort(
        (a: any, b: any) =>
          new Date(b.created_at).getTime() -
          new Date(a.created_at).getTime()
      )
    );
  } catch (err) {
    console.error(err);
    setHistory([]);
  } finally {
    setLoadingHistory(false);
  }
};

  useEffect(() => { fetchHistory(); }, [isAuthenticated]);

  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Text style={styles.title}>Product Search</Text>

      <View style={styles.card}>
        {!selectedImage ? (
          <TouchableOpacity style={styles.mainButton} onPress={() => Alert.alert("Select Image Source", "", [
            { text: "Take Photo", onPress: takePhoto },
            { text: "Choose from Gallery", onPress: pickFromGallery },
            { text: "Cancel", style: "cancel" },
          ])}>
            <MaterialCommunityIcons name="camera-plus" size={24} color="white"/>
            <Text style={styles.buttonText}>Upload / Take Picture</Text>
          </TouchableOpacity>
        ) : (
          <View style={styles.imageContainer}>
            <Image source={{ uri: selectedImage }} style={styles.selectedImage} />
            <View style={styles.buttonRow}>
              <TouchableOpacity style={[styles.actionButton, styles.changeButton]} onPress={() => setSelectedImage(null)}>
                <MaterialCommunityIcons name="image-edit" size={20} color="white"/>
                <Text style={styles.actionButtonText}>Change</Text>
              </TouchableOpacity>
              <TouchableOpacity style={[styles.actionButton, styles.uploadButton]} onPress={uploadImage} disabled={isUploading}>
                {isUploading ? <ActivityIndicator color="white"/> : <>
                  <MaterialCommunityIcons name="cloud-upload" size={20} color="white"/>
                  <Text style={styles.actionButtonText}>Upload</Text>
                </>}
              </TouchableOpacity>
            </View>
          </View>
        )}
      </View>

      <View style={styles.card}>
        <Text style={styles.sectionTitle}>History</Text>

        {isAuthenticated ? (
          <>
            <TouchableOpacity style={[styles.mainButton, { marginBottom: 12 }]} onPress={fetchHistory}>
              <MaterialCommunityIcons name="refresh" size={20} color="white"/>
              <Text style={styles.buttonText}>Refresh History</Text>
            </TouchableOpacity>

            {loadingHistory ? (
              <ActivityIndicator color="white" size="large"/>
) : history.length === 0 ? (
  <Text style={styles.placeholderText}>No history yet...</Text>
) : (
history.map(item => {
  return (
    <TouchableOpacity
      key={item.req
[truncated — 3428 more characters]
```

### frontend/eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

### backend/example-server.js

```javascript

const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();
const PORT = 3000;

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/'); 
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
  }
});

const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024, 
  },
  fileFilter: (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Only image files are allowed!'), false);
    }
  }
});

app.post('/api/upload', upload.single('image'), (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No image file provided' });
    }


    const imageInfo = {
      filename: req.file.filename,
      originalName: req.file.originalname,
      size: req.file.size,
      mimetype: req.file.mimetype,
      path: req.file.path,
      uploadedAt: new Date().toISOString()
    };

    res.json({
      success: true,
      message: 'Image uploaded successfully',
      image: imageInfo,
      analysis: {
      }
    });

  } catch (error) {
    console.error('Upload error:', error);
    res.status(500).json({
      error: 'Failed to upload image',
      details: error.message
    });
  }
});

app.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    if (error.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large. Maximum size is 5MB.' });
    }
  }

  res.status(500).json({ error: error.message });
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});


```

[25 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]