prettier !

This commit is contained in:
2025-12-23 17:57:32 -08:00
parent 2334359756
commit 6c8fd4b126
21 changed files with 225 additions and 208 deletions

View File

@@ -6,4 +6,4 @@
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"endOfLine": "auto" "endOfLine": "auto"
} }

130
README.md
View File

@@ -2,7 +2,7 @@
Flyer Crawler is a web application that uses the Google Gemini AI to extract, analyze, and manage data from grocery store flyers. Users can upload flyer images or PDFs, and the application will automatically identify items, prices, and sale dates, storing the structured data in a PostgreSQL database for historical analysis, price tracking, and personalized deal alerts. Flyer Crawler is a web application that uses the Google Gemini AI to extract, analyze, and manage data from grocery store flyers. Users can upload flyer images or PDFs, and the application will automatically identify items, prices, and sale dates, storing the structured data in a PostgreSQL database for historical analysis, price tracking, and personalized deal alerts.
We are working on an app to help people save money, by finding good deals that are only advertized in store flyers/ads. So, the primary purpose of the site is to make uploading flyers as easy as possible and as accurate as possible, and to store peoples needs, so sales can be matched to needs. We are working on an app to help people save money, by finding good deals that are only advertized in store flyers/ads. So, the primary purpose of the site is to make uploading flyers as easy as possible and as accurate as possible, and to store peoples needs, so sales can be matched to needs.
## Features ## Features
@@ -45,9 +45,9 @@ This project is configured to run in a CI/CD environment and does not use `.env`
1. **Set up a PostgreSQL database instance.** 1. **Set up a PostgreSQL database instance.**
2. **Run the Database Schema**: 2. **Run the Database Schema**:
- Connect to your database using a tool like `psql` or DBeaver. - Connect to your database using a tool like `psql` or DBeaver.
- Open `sql/schema.sql.txt`, copy its entire contents, and execute it against your database. - Open `sql/schema.sql.txt`, copy its entire contents, and execute it against your database.
- This will create all necessary tables, functions, and relationships. - This will create all necessary tables, functions, and relationships.
### Step 2: Install Dependencies and Run the Application ### Step 2: Install Dependencies and Run the Application
@@ -79,11 +79,11 @@ sudo nano /etc/nginx/mime.types
change change
application/javascript js; application/javascript js;
TO TO
application/javascript js mjs; application/javascript js mjs;
RESTART NGINX RESTART NGINX
@@ -95,7 +95,7 @@ actually the proper change was to do this in the /etc/nginx/sites-available/flye
## for OAuth ## for OAuth
1. Get Google OAuth Credentials 1. Get Google OAuth Credentials
This is a crucial step that you must do outside the codebase: This is a crucial step that you must do outside the codebase:
Go to the Google Cloud Console. Go to the Google Cloud Console.
@@ -112,7 +112,7 @@ Under Authorized redirect URIs, click ADD URI and enter the URL where Google wil
Click Create. You will be given a Client ID and a Client Secret. Click Create. You will be given a Client ID and a Client Secret.
2. Get GitHub OAuth Credentials 2. Get GitHub OAuth Credentials
You'll need to obtain a Client ID and Client Secret from GitHub: You'll need to obtain a Client ID and Client Secret from GitHub:
Go to your GitHub profile settings. Go to your GitHub profile settings.
@@ -133,21 +133,23 @@ You will be given a Client ID and a Client Secret.
psql -h localhost -U flyer_crawler_user -d "flyer-crawler-prod" -W psql -h localhost -U flyer_crawler_user -d "flyer-crawler-prod" -W
## postgis ## postgis
flyer-crawler-prod=> SELECT version(); flyer-crawler-prod=> SELECT version();
version version
------------------------------------------------------------------------------------------------------------------------------------------
PostgreSQL 14.19 (Ubuntu 14.19-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0, 64-bit ---
PostgreSQL 14.19 (Ubuntu 14.19-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0, 64-bit
(1 row) (1 row)
flyer-crawler-prod=> SELECT PostGIS_Full_Version(); flyer-crawler-prod=> SELECT PostGIS_Full_Version();
postgis_full_version postgis_full_version
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
POSTGIS="3.2.0 c3e3cc0" [EXTENSION] PGSQL="140" GEOS="3.10.2-CAPI-1.16.0" PROJ="8.2.1" LIBXML="2.9.12" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0 (Internal)"
(1 row)
---
POSTGIS="3.2.0 c3e3cc0" [EXTENSION] PGSQL="140" GEOS="3.10.2-CAPI-1.16.0" PROJ="8.2.1" LIBXML="2.9.12" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0 (Internal)"
(1 row)
## production postgres setup ## production postgres setup
@@ -201,9 +203,13 @@ Step 4: Seed the Admin Account (If Needed)
Your application has a separate script to create the initial admin user. To run it, you must first set the required environment variables in your shell session. Your application has a separate script to create the initial admin user. To run it, you must first set the required environment variables in your shell session.
bash bash
# Set variables for the current session # Set variables for the current session
export DB_USER=flyer_crawler_user DB_PASSWORD=your_password DB_NAME="flyer-crawler-prod" ... export DB_USER=flyer_crawler_user DB_PASSWORD=your_password DB_NAME="flyer-crawler-prod" ...
# Run the seeding script # Run the seeding script
npx tsx src/db/seed_admin_account.ts npx tsx src/db/seed_admin_account.ts
Your production database is now ready! Your production database is now ready!
@@ -284,8 +290,6 @@ Test Execution: Your tests run against this clean, isolated schema.
This approach is faster, more reliable, and removes the need for sudo access within the CI pipeline. This approach is faster, more reliable, and removes the need for sudo access within the CI pipeline.
gitea-runner@projectium:~$ pm2 install pm2-logrotate gitea-runner@projectium:~$ pm2 install pm2-logrotate
[PM2][Module] Installing NPM pm2-logrotate module [PM2][Module] Installing NPM pm2-logrotate module
[PM2][Module] Calling [NPM] to install pm2-logrotate ... [PM2][Module] Calling [NPM] to install pm2-logrotate ...
@@ -293,7 +297,7 @@ gitea-runner@projectium:~$ pm2 install pm2-logrotate
added 161 packages in 5s added 161 packages in 5s
21 packages are looking for funding 21 packages are looking for funding
run `npm fund` for details run `npm fund` for details
npm notice npm notice
npm notice New patch version of npm available! 11.6.3 -> 11.6.4 npm notice New patch version of npm available! 11.6.3 -> 11.6.4
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.6.4 npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.6.4
@@ -308,23 +312,23 @@ $ pm2 set pm2-logrotate:retain 30
$ pm2 set pm2-logrotate:compress false $ pm2 set pm2-logrotate:compress false
$ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss $ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
$ pm2 set pm2-logrotate:workerInterval 30 $ pm2 set pm2-logrotate:workerInterval 30
$ pm2 set pm2-logrotate:rotateInterval 0 0 * * * $ pm2 set pm2-logrotate:rotateInterval 0 0 \* \* _
$ pm2 set pm2-logrotate:rotateModule true $ pm2 set pm2-logrotate:rotateModule true
Modules configuration. Copy/Paste line to edit values. Modules configuration. Copy/Paste line to edit values.
[PM2][Module] Module successfully installed and launched [PM2][Module] Module successfully installed and launched
[PM2][Module] Checkout module options: `$ pm2 conf` [PM2][Module] Checkout module options: `$ pm2 conf`
┌────┬───────────────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ ┌────┬───────────────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────────────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ ├────┼───────────────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 2 │ flyer-crawler-analytics-worker │ default │ 0.0.0 │ fork │ 3846981 │ 7m │ 5 │ online │ 0% │ 55.8mb │ git… │ disabled │ │ 2 │ flyer-crawler-analytics-worker │ default │ 0.0.0 │ fork │ 3846981 │ 7m │ 5 │ online │ 0% │ 55.8mb │ git… │ disabled │
│ 11 │ flyer-crawler-api │ default │ 0.0.0 │ fork │ 3846987 │ 7m │ 0 │ online │ 0% │ 59.0mb │ git… │ disabled │ │ 11 │ flyer-crawler-api │ default │ 0.0.0 │ fork │ 3846987 │ 7m │ 0 │ online │ 0% │ 59.0mb │ git… │ disabled │
│ 12 │ flyer-crawler-worker │ default │ 0.0.0 │ fork │ 3846988 │ 7m │ 0 │ online │ 0% │ 54.2mb │ git… │ disabled │ │ 12 │ flyer-crawler-worker │ default │ 0.0.0 │ fork │ 3846988 │ 7m │ 0 │ online │ 0% │ 54.2mb │ git… │ disabled │
└────┴───────────────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ └────┴───────────────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Module Module
┌────┬──────────────────────────────┬───────────────┬──────────┬──────────┬──────┬──────────┬──────────┬──────────┐ ┌────┬──────────────────────────────┬───────────────┬──────────┬──────────┬──────┬──────────┬──────────┬──────────┐
│ id │ module │ version │ pid │ status │ ↺ │ cpu │ mem │ user │ id │ module │ version │ pid │ status │ ↺ │ cpu │ mem │ user │
├────┼──────────────────────────────┼───────────────┼──────────┼──────────┼──────┼──────────┼──────────┼──────────┤ ├────┼──────────────────────────────┼───────────────┼──────────┼──────────┼──────┼──────────┼──────────┼──────────┤
│ 13 │ pm2-logrotate │ 3.0.0 │ 3848878 │ online │ 0 │ 0% │ 20.1mb │ git… │ │ 13 │ pm2-logrotate │ 3.0.0 │ 3848878 │ online │ 0 │ 0% │ 20.1mb │ git… │
└────┴──────────────────────────────┴───────────────┴──────────┴──────────┴──────┴──────────┴──────────┴──────────┘ └────┴──────────────────────────────┴───────────────┴──────────┴──────────┴──────┴──────────┴──────────┴──────────┘
gitea-runner@projectium:~$ pm2 set pm2-logrotate:max_size 10M gitea-runner@projectium:~$ pm2 set pm2-logrotate:max_size 10M
[PM2] Module pm2-logrotate restarted [PM2] Module pm2-logrotate restarted
@@ -335,7 +339,7 @@ $ pm2 set pm2-logrotate:retain 30
$ pm2 set pm2-logrotate:compress false $ pm2 set pm2-logrotate:compress false
$ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss $ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
$ pm2 set pm2-logrotate:workerInterval 30 $ pm2 set pm2-logrotate:workerInterval 30
$ pm2 set pm2-logrotate:rotateInterval 0 0 * * * $ pm2 set pm2-logrotate:rotateInterval 0 0 _ \* _
$ pm2 set pm2-logrotate:rotateModule true $ pm2 set pm2-logrotate:rotateModule true
gitea-runner@projectium:~$ pm2 set pm2-logrotate:retain 14 gitea-runner@projectium:~$ pm2 set pm2-logrotate:retain 14
[PM2] Module pm2-logrotate restarted [PM2] Module pm2-logrotate restarted
@@ -346,33 +350,31 @@ $ pm2 set pm2-logrotate:retain 14
$ pm2 set pm2-logrotate:compress false $ pm2 set pm2-logrotate:compress false
$ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss $ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
$ pm2 set pm2-logrotate:workerInterval 30 $ pm2 set pm2-logrotate:workerInterval 30
$ pm2 set pm2-logrotate:rotateInterval 0 0 * * * $ pm2 set pm2-logrotate:rotateInterval 0 0 _ \* \*
$ pm2 set pm2-logrotate:rotateModule true $ pm2 set pm2-logrotate:rotateModule true
gitea-runner@projectium:~$ gitea-runner@projectium:~$
## dev server setup: ## dev server setup:
Here are the steps to set up the development environment on Windows using Podman with an Ubuntu container: Here are the steps to set up the development environment on Windows using Podman with an Ubuntu container:
1. Install Prerequisites on Windows 1. Install Prerequisites on Windows
Install WSL 2: Podman on Windows relies on the Windows Subsystem for Linux. Install it by running wsl --install in an administrator PowerShell. Install WSL 2: Podman on Windows relies on the Windows Subsystem for Linux. Install it by running wsl --install in an administrator PowerShell.
Install Podman Desktop: Download and install Podman Desktop for Windows. Install Podman Desktop: Download and install Podman Desktop for Windows.
2. Set Up Podman 2. Set Up Podman
Initialize Podman: Launch Podman Desktop. It will automatically set up its WSL 2 machine. Initialize Podman: Launch Podman Desktop. It will automatically set up its WSL 2 machine.
Start Podman: Ensure the Podman machine is running from the Podman Desktop interface. Start Podman: Ensure the Podman machine is running from the Podman Desktop interface.
3. Set Up the Ubuntu Container 3. Set Up the Ubuntu Container
- Pull Ubuntu Image: Open a PowerShell or command prompt and pull the latest Ubuntu image:
podman pull ubuntu:latest - Pull Ubuntu Image: Open a PowerShell or command prompt and pull the latest Ubuntu image:
- Create a Podman Volume: Create a volume to persist node_modules and avoid installing them every time the container starts. podman pull ubuntu:latest
podman volume create node_modules_cache - Create a Podman Volume: Create a volume to persist node_modules and avoid installing them every time the container starts.
- Run the Ubuntu Container: Start a new container with the project directory mounted and the necessary ports forwarded. podman volume create node_modules_cache
- Open a terminal in your project's root directory on Windows. - Run the Ubuntu Container: Start a new container with the project directory mounted and the necessary ports forwarded.
- Run the following command, replacing D:\gitea\flyer-crawler.projectium.com\flyer-crawler.projectium.com with the full path to your project: - Open a terminal in your project's root directory on Windows.
- Run the following command, replacing D:\gitea\flyer-crawler.projectium.com\flyer-crawler.projectium.com with the full path to your project:
podman run -it -p 3001:3001 -p 5173:5173 --name flyer-dev -v "D:\gitea\flyer-crawler.projectium.com\flyer-crawler.projectium.com:/app" -v "node_modules_cache:/app/node_modules" ubuntu:latest podman run -it -p 3001:3001 -p 5173:5173 --name flyer-dev -v "D:\gitea\flyer-crawler.projectium.com\flyer-crawler.projectium.com:/app" -v "node_modules_cache:/app/node_modules" ubuntu:latest
@@ -383,46 +385,40 @@ podman run -it -p 3001:3001 -p 5173:5173 --name flyer-dev -v "D:\gitea\flyer-cra
-v "node_modules_cache:/app/node_modules": Mounts the named volume for node_modules. -v "node_modules_cache:/app/node_modules": Mounts the named volume for node_modules.
4. Configure the Ubuntu Environment 4. Configure the Ubuntu Environment
You are now inside the Ubuntu container's shell. You are now inside the Ubuntu container's shell.
- Update Package Lists: - Update Package Lists:
apt-get update apt-get update
- Install Dependencies: Install curl, git, and nodejs (which includes npm). - Install Dependencies: Install curl, git, and nodejs (which includes npm).
apt-get install -y curl git apt-get install -y curl git
curl -sL https://deb.nodesource.com/setup_20.x | bash - curl -sL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs apt-get install -y nodejs
- Navigate to Project Directory: - Navigate to Project Directory:
cd /app cd /app
- Install Project Dependencies: - Install Project Dependencies:
npm install npm install
5. Run the Development Server 5. Run the Development Server
- Start the Application: - Start the Application:
npm run dev npm run dev
6. Accessing the Application 6. Accessing the Application
- Frontend: Open your browser and go to http://localhost:5173.
- Backend: The frontend will make API calls to http://localhost:3001. - Frontend: Open your browser and go to http://localhost:5173.
- Backend: The frontend will make API calls to http://localhost:3001.
Managing the Environment Managing the Environment
- Stopping the Container: Press Ctrl+C in the container terminal, then type exit.
- Restarting the Container:
podman start -a -i flyer-dev
- Stopping the Container: Press Ctrl+C in the container terminal, then type exit.
- Restarting the Container:
podman start -a -i flyer-dev
## for me: ## for me:
cd /mnt/d/gitea/flyer-crawler.projectium.com/flyer-crawler.projectium.com cd /mnt/d/gitea/flyer-crawler.projectium.com/flyer-crawler.projectium.com
podman run -it -p 3001:3001 -p 5173:5173 --name flyer-dev -v "$(pwd):/app" -v "node_modules_cache:/app/node_modules" ubuntu:latest podman run -it -p 3001:3001 -p 5173:5173 --name flyer-dev -v "$(pwd):/app" -v "node_modules_cache:/app/node_modules" ubuntu:latest
rate limiting rate limiting
respect the AI service's rate limits, making it more stable and robust. You can adjust the GEMINI_RPM environment variable in your production environment as needed without changing the code. respect the AI service's rate limits, making it more stable and robust. You can adjust the GEMINI_RPM environment variable in your production environment as needed without changing the code.

View File

@@ -15,7 +15,7 @@ module.exports = {
args: 'server.ts', // tsx will execute this file args: 'server.ts', // tsx will execute this file
// Production Environment Settings // Production Environment Settings
env_production: { env_production: {
NODE_ENV: 'production', // Set the Node.js environment to production NODE_ENV: 'production', // Set the Node.js environment to production
name: 'flyer-crawler-api', name: 'flyer-crawler-api',
cwd: '/var/www/flyer-crawler.projectium.com', cwd: '/var/www/flyer-crawler.projectium.com',
}, },
@@ -63,4 +63,4 @@ module.exports = {
}, },
}, },
], ],
}; };

View File

@@ -1,21 +1,21 @@
import globals from "globals"; import globals from 'globals';
import tseslint from "typescript-eslint"; import tseslint from 'typescript-eslint';
import pluginReact from "eslint-plugin-react"; import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from "eslint-plugin-react-hooks"; import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginReactRefresh from "eslint-plugin-react-refresh"; import pluginReactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config( export default tseslint.config(
{ {
// Global ignores // Global ignores
ignores: ["dist", ".gitea", "node_modules", "*.cjs"], ignores: ['dist', '.gitea', 'node_modules', '*.cjs'],
}, },
{ {
// All files // All files
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
plugins: { plugins: {
react: pluginReact, react: pluginReact,
"react-hooks": pluginReactHooks, 'react-hooks': pluginReactHooks,
"react-refresh": pluginReactRefresh, 'react-refresh': pluginReactRefresh,
}, },
languageOptions: { languageOptions: {
globals: { globals: {
@@ -24,12 +24,9 @@ export default tseslint.config(
}, },
}, },
rules: { rules: {
"react-refresh/only-export-components": [ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
"warn",
{ allowConstantExport: true },
],
}, },
}, },
// TypeScript files // TypeScript files
...tseslint.configs.recommended, ...tseslint.configs.recommended,
); );

4
express.d.ts vendored
View File

@@ -1,4 +1,4 @@
// src/types/express.d.ts // express.d.ts
import { Logger } from 'pino'; import { Logger } from 'pino';
/** /**
@@ -12,4 +12,4 @@ declare global {
log: Logger; log: Logger;
} }
} }
} }

View File

@@ -1,20 +1,20 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grocery Flyer AI Analyzer</title> <title>Grocery Flyer AI Analyzer</title>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }
</style> </style>
<!-- The stylesheet will be injected here by Vite during the build process --> <!-- The stylesheet will be injected here by Vite during the build process -->
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<!-- Vite will inject the correct <script> tag here during the build process --> <!-- Vite will inject the correct <script> tag here during the build process -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,8 +1,5 @@
{ {
"name": "Flyer Crawler", "name": "Flyer Crawler",
"description": "Upload a grocery store flyer image to extract item details, prices, and quantities using AI. Get insights, meal plans, and compare prices to save money on your shopping.", "description": "Upload a grocery store flyer image to extract item details, prices, and quantities using AI. Get insights, meal plans, and compare prices to save money on your shopping.",
"requestFramePermissions": [ "requestFramePermissions": ["geolocation", "microphone"]
"geolocation", }
"microphone"
]
}

View File

@@ -10,10 +10,13 @@ const tailwindConfigPath = path.resolve(process.cwd(), 'tailwind.config.js');
console.log(`[POSTCSS] Attempting to use Tailwind config at: ${tailwindConfigPath}`); console.log(`[POSTCSS] Attempting to use Tailwind config at: ${tailwindConfigPath}`);
// Log to prove the imported config object is what we expect // Log to prove the imported config object is what we expect
console.log('[POSTCSS] Imported tailwind.config.js object:', JSON.stringify(tailwindConfig, null, 2)); console.log(
'[POSTCSS] Imported tailwind.config.js object:',
JSON.stringify(tailwindConfig, null, 2),
);
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, // The empty object is correct. '@tailwindcss/postcss': {}, // The empty object is correct.
}, },
}; };

View File

@@ -16,4 +16,4 @@ const config = {
}, },
}; };
export default config; export default config;

View File

@@ -4,7 +4,7 @@
This single directive replaces @tailwind base, components, and utilities. This single directive replaces @tailwind base, components, and utilities.
It is the new entry point for all of Tailwind's generated CSS. It is the new entry point for all of Tailwind's generated CSS.
*/ */
@import "tailwindcss"; @import 'tailwindcss';
/* /*
This is the new v4 directive that tells the @tailwindcss/postcss plugin This is the new v4 directive that tells the @tailwindcss/postcss plugin
@@ -12,4 +12,3 @@
Since tailwind.config.js is in the root and this is in src/, the path is '../tailwind.config.js'. Since tailwind.config.js is in the root and this is in src/, the path is '../tailwind.config.js'.
*/ */
@config '../tailwind.config.js'; @config '../tailwind.config.js';

View File

@@ -8,17 +8,16 @@ import './index.css';
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
if (!rootElement) { if (!rootElement) {
throw new Error("Could not find root element to mount to"); throw new Error('Could not find root element to mount to');
} }
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<AppProviders> <AppProviders>
<App /> <App />
</AppProviders> </AppProviders>
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>,
); );

View File

@@ -5,4 +5,4 @@ import toast from 'react-hot-toast';
// This intermediate file allows us to mock 'src/lib/toast' reliably in tests // This intermediate file allows us to mock 'src/lib/toast' reliably in tests
// without wrestling with the internal structure of the 'react-hot-toast' package. // without wrestling with the internal structure of the 'react-hot-toast' package.
export * from 'react-hot-toast'; export * from 'react-hot-toast';
export default toast; export default toast;

View File

@@ -52,7 +52,10 @@ export type FlyerDbInsert = Omit<FlyerInsert, 'store_name'> & { store_id: number
* Represents the data required to insert a new flyer item into the database. * Represents the data required to insert a new flyer item into the database.
* It's a subset of the full FlyerItem type. * It's a subset of the full FlyerItem type.
*/ */
export type FlyerItemInsert = Omit<FlyerItem, 'flyer_item_id' | 'flyer_id' | 'created_at' | 'updated_at'>; export type FlyerItemInsert = Omit<
FlyerItem,
'flyer_item_id' | 'flyer_id' | 'created_at' | 'updated_at'
>;
export interface UnitPrice { export interface UnitPrice {
value: number; value: number;
@@ -163,13 +166,12 @@ export interface Profile {
updated_by?: string | null; updated_by?: string | null;
} }
/** /**
* Represents the combined user and profile data object returned by the backend's /users/profile endpoint. * Represents the combined user and profile data object returned by the backend's /users/profile endpoint.
* It embeds the User object within the Profile object. * It embeds the User object within the Profile object.
* It also includes the full Address object if one is associated with the profile. * It also includes the full Address object if one is associated with the profile.
*/ */
export type UserProfile = Profile & { export type UserProfile = Profile & {
user: User; user: User;
address?: Address | null; address?: Address | null;
}; };
@@ -325,7 +327,6 @@ export interface RecipeIngredientSubstitution {
notes?: string | null; notes?: string | null;
} }
export interface Tag { export interface Tag {
tag_id: number; tag_id: number;
name: string; name: string;
@@ -718,7 +719,10 @@ export type AiAnalysisAction =
// Dispatched when an analysis that returns a simple string succeeds. // Dispatched when an analysis that returns a simple string succeeds.
| { type: 'FETCH_SUCCESS_TEXT'; payload: { analysisType: AnalysisType; data: string } } | { type: 'FETCH_SUCCESS_TEXT'; payload: { analysisType: AnalysisType; data: string } }
// Dispatched when an analysis that returns text and sources succeeds. // Dispatched when an analysis that returns text and sources succeeds.
| { type: 'FETCH_SUCCESS_GROUNDED'; payload: { analysisType: AnalysisType; data: GroundedResponse } } | {
type: 'FETCH_SUCCESS_GROUNDED';
payload: { analysisType: AnalysisType; data: GroundedResponse };
}
// Dispatched when the image generation succeeds. // Dispatched when the image generation succeeds.
| { type: 'FETCH_SUCCESS_IMAGE'; payload: { data: string } } | { type: 'FETCH_SUCCESS_IMAGE'; payload: { data: string } }
// Dispatched when any analysis fails. // Dispatched when any analysis fails.
@@ -738,11 +742,25 @@ export interface ProcessingStage {
} }
export const CATEGORIES = [ export const CATEGORIES = [
'Fruits & Vegetables', 'Meat & Seafood', 'Dairy & Eggs', 'Bakery & Bread', 'Fruits & Vegetables',
'Pantry & Dry Goods', 'Beverages', 'Frozen Foods', 'Snacks', 'Household & Cleaning', 'Meat & Seafood',
'Personal Care & Health', 'Baby & Child', 'Pet Supplies', 'Deli & Prepared Foods', 'Dairy & Eggs',
'Canned Goods', 'Condiments & Spices', 'Breakfast & Cereal', 'Organic', 'Bakery & Bread',
'International Foods', 'Other/Miscellaneous' 'Pantry & Dry Goods',
'Beverages',
'Frozen Foods',
'Snacks',
'Household & Cleaning',
'Personal Care & Health',
'Baby & Child',
'Pet Supplies',
'Deli & Prepared Foods',
'Canned Goods',
'Condiments & Spices',
'Breakfast & Cereal',
'Organic',
'International Foods',
'Other/Miscellaneous',
]; ];
/** /**
@@ -750,11 +768,11 @@ export const CATEGORIES = [
* This is the structure returned from the backend to the frontend. * This is the structure returned from the backend to the frontend.
*/ */
export interface ExtractedCoreData { export interface ExtractedCoreData {
store_name: string; store_name: string;
valid_from: string | null; valid_from: string | null;
valid_to: string | null; valid_to: string | null;
store_address: string | null; store_address: string | null;
items: ExtractedFlyerItem[]; items: ExtractedFlyerItem[];
} }
/** /**
@@ -776,19 +794,19 @@ export interface ExtractedFlyerItem {
* Represents the logo data extracted from a flyer by the AI service. * Represents the logo data extracted from a flyer by the AI service.
*/ */
export interface ExtractedLogoData { export interface ExtractedLogoData {
store_logo_base_64: string | null; store_logo_base_64: string | null;
} }
/** /**
* Represents the data extracted from a receipt image by the AI service. * Represents the data extracted from a receipt image by the AI service.
*/ */
export interface ExtractedReceiptData { export interface ExtractedReceiptData {
raw_text: string; raw_text: string;
items: { items: {
raw_item_description: string; raw_item_description: string;
quantity: number; quantity: number;
price_paid_cents: number; price_paid_cents: number;
}[]; }[];
} }
/** /**
@@ -930,10 +948,10 @@ export interface LeaderboardUser {
* This is a public-facing type and does not include sensitive fields. * This is a public-facing type and does not include sensitive fields.
*/ */
export interface AdminUserView { export interface AdminUserView {
user_id: string; user_id: string;
email: string; email: string;
created_at: string; created_at: string;
role: 'admin' | 'user'; role: 'admin' | 'user';
full_name: string | null; full_name: string | null;
avatar_url: string | null; avatar_url: string | null;
} }

2
src/vite-env.d.ts vendored
View File

@@ -10,4 +10,4 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }

View File

@@ -2,4 +2,4 @@
// This file can be used for global setup logic that applies to ALL test projects // This file can be used for global setup logic that applies to ALL test projects
// defined in the workspace. Since our unit and integration tests have distinct // defined in the workspace. Since our unit and integration tests have distinct
// setup requirements, this file is currently empty. // setup requirements, this file is currently empty.

View File

@@ -4,8 +4,5 @@ console.log('--- [EXECUTION PROOF] tailwind.config.js is being loaded. ---');
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
"./index.html", };
"./src/**/*.{js,ts,jsx,tsx}",
],
};

View File

@@ -17,12 +17,10 @@
"jsx": "react-jsx", "jsx": "react-jsx",
// This line makes Vitest's global APIs (describe, it, expect) available everywhere // This line makes Vitest's global APIs (describe, it, expect) available everywhere
// without needing to import them. // without needing to import them.
"types": [ "types": ["vitest/globals"]
"vitest/globals"
]
}, },
// This is the most important part: It tells TypeScript to include ALL files // This is the most important part: It tells TypeScript to include ALL files
// within the 'src' directory, including your new 'vite-env.d.ts' file. // within the 'src' directory, including your new 'vite-env.d.ts' file.
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@@ -9,5 +9,10 @@
"strict": true, // It's good practice to keep tooling config strict "strict": true, // It's good practice to keep tooling config strict
"types": ["node"] "types": ["node"]
}, },
"include": ["vite.config.ts", "vitest.config.ts", "vitest.config.integration.ts", "vitest.workspace.ts"] "include": [
} "vite.config.ts",
"vitest.config.ts",
"vitest.config.integration.ts",
"vitest.workspace.ts"
]
}

View File

@@ -22,7 +22,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
// Use __dirname for a more robust path resolution // Use __dirname for a more robust path resolution
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
// This alias ensures that any import of 'services/logger' is resolved // This alias ensures that any import of 'services/logger' is resolved
// to the browser-safe client version during the Vite build process. // to the browser-safe client version during the Vite build process.
// Server-side code should explicitly import 'services/logger.server'. // Server-side code should explicitly import 'services/logger.server'.
@@ -51,7 +51,7 @@ export default defineConfig({
'**/node_modules/**', '**/node_modules/**',
'**/dist/**', '**/dist/**',
'src/tests/integration/**', // Exclude the entire integration test directory 'src/tests/integration/**', // Exclude the entire integration test directory
'**/*.e2e.test.ts' '**/*.e2e.test.ts',
], ],
// Disable file parallelism to run tests sequentially (replaces --no-threads) // Disable file parallelism to run tests sequentially (replaces --no-threads)
fileParallelism: false, fileParallelism: false,
@@ -60,10 +60,12 @@ export default defineConfig({
reporter: [ reporter: [
// Add maxCols to suggest a wider output for the text summary. // Add maxCols to suggest a wider output for the text summary.
['text', { maxCols: 200 }], ['text', { maxCols: 200 }],
'html', 'json'], 'html',
'json',
],
// hanging-process reporter helps identify tests that do not exit properly - comes at a high cost tho // hanging-process reporter helps identify tests that do not exit properly - comes at a high cost tho
//reporter: ['verbose', 'html', 'json', 'hanging-process'], //reporter: ['verbose', 'html', 'json', 'hanging-process'],
reportsDirectory: './.coverage/unit', reportsDirectory: './.coverage/unit',
clean: true, clean: true,
reportOnFailure: true, // This ensures the report generates even if tests fail reportOnFailure: true, // This ensures the report generates even if tests fail
include: ['src/**/*.{ts,tsx}'], include: ['src/**/*.{ts,tsx}'],
@@ -71,19 +73,19 @@ export default defineConfig({
// By excluding scripts, setup files, and type definitions, we get a more accurate // By excluding scripts, setup files, and type definitions, we get a more accurate
// picture of the test coverage for the actual application logic. // picture of the test coverage for the actual application logic.
exclude: [ exclude: [
'src/index.tsx', // Application entry point 'src/index.tsx', // Application entry point
'src/main.tsx', // A common alternative entry point name 'src/main.tsx', // A common alternative entry point name
'src/types.ts', 'src/types.ts',
'src/tests/**', // Exclude all test setup and helper files 'src/tests/**', // Exclude all test setup and helper files
'src/vitest.setup.ts', // Global test setup config 'src/vitest.setup.ts', // Global test setup config
'src/**/*.test.{ts,tsx}', // Exclude test files themselves 'src/**/*.test.{ts,tsx}', // Exclude test files themselves
'src/**/*.stories.{ts,tsx}', // Exclude Storybook stories 'src/**/*.stories.{ts,tsx}', // Exclude Storybook stories
'src/**/*.d.ts', // Exclude type definition files 'src/**/*.d.ts', // Exclude type definition files
'src/components/icons/**', // Exclude icon components if they are simple wrappers 'src/components/icons/**', // Exclude icon components if they are simple wrappers
'src/db/seed.ts', // Database seeding script 'src/db/seed.ts', // Database seeding script
'src/db/seed_admin_account.ts', // Database seeding script 'src/db/seed_admin_account.ts', // Database seeding script
'src/db/backup_user.ts', // Database backup script 'src/db/backup_user.ts', // Database backup script
], ],
}, },
}, },
}); });

View File

@@ -6,7 +6,7 @@ import viteConfig from './vite.config';
// Ensure NODE_ENV is set to 'test' for all Vitest runs. // Ensure NODE_ENV is set to 'test' for all Vitest runs.
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
// 1. Separate the 'test' config (which has Unit Test settings) // 1. Separate the 'test' config (which has Unit Test settings)
// from the rest of the general Vite config (plugins, aliases, etc.) // from the rest of the general Vite config (plugins, aliases, etc.)
// DEBUG: Use console.error to ensure logs appear in CI/CD output // DEBUG: Use console.error to ensure logs appear in CI/CD output
console.error('[DEBUG] Loading vitest.config.integration.ts...'); console.error('[DEBUG] Loading vitest.config.integration.ts...');
@@ -30,38 +30,41 @@ console.error('[DEBUG] Base vite config keys:', Object.keys(baseViteConfig));
* It MERGES with the main vite.config.ts to inherit plugins and aliases, * It MERGES with the main vite.config.ts to inherit plugins and aliases,
* then overrides the test-specific settings for a Node.js environment. * then overrides the test-specific settings for a Node.js environment.
*/ */
const finalConfig = mergeConfig(baseViteConfig, defineConfig({ const finalConfig = mergeConfig(
test: { baseViteConfig,
// Override settings from the main config for this specific test project. defineConfig({
name: 'integration', test: {
environment: 'node', // Override settings from the main config for this specific test project.
// Point specifically to the new integration tests directory. name: 'integration',
// This pattern will match any test file inside `src/tests/integration/`. environment: 'node',
include: ['src/tests/integration/**/*.test.{ts,tsx}'], // Point specifically to the new integration tests directory.
// CRITICAL: We must override the `exclude` property from the base vite.config.ts. // This pattern will match any test file inside `src/tests/integration/`.
// Otherwise, the inherited `exclude` rule will prevent any integration tests from running. include: ['src/tests/integration/**/*.test.{ts,tsx}'],
// Setting it to an empty array removes all exclusion rules for this project. // CRITICAL: We must override the `exclude` property from the base vite.config.ts.
exclude: [], // Otherwise, the inherited `exclude` rule will prevent any integration tests from running.
// This setup script starts the backend server before tests run. // Setting it to an empty array removes all exclusion rules for this project.
globalSetup: './src/tests/setup/integration-global-setup.ts', exclude: [],
// The default timeout is 5000ms (5 seconds) // This setup script starts the backend server before tests run.
testTimeout: 60000, // Increased timeout for server startup and API calls, especially AI services. globalSetup: './src/tests/setup/integration-global-setup.ts',
// "singleThread: true" is removed in modern Vitest. // The default timeout is 5000ms (5 seconds)
// Use fileParallelism: false to ensure test files run one by one to prevent port conflicts. testTimeout: 60000, // Increased timeout for server startup and API calls, especially AI services.
fileParallelism: false, // "singleThread: true" is removed in modern Vitest.
coverage: { // Use fileParallelism: false to ensure test files run one by one to prevent port conflicts.
provider: 'v8', fileParallelism: false,
// We remove 'text' here. The final text report will be generated by `nyc` after merging. coverage: {
reporter: ['html', 'json-summary', 'json'], provider: 'v8',
reportsDirectory: '.coverage/integration', // We remove 'text' here. The final text report will be generated by `nyc` after merging.
reportOnFailure: true, // This ensures the report generates even if tests fail reporter: ['html', 'json-summary', 'json'],
clean: true, reportsDirectory: '.coverage/integration',
reportOnFailure: true, // This ensures the report generates even if tests fail
clean: true,
},
}, },
} }),
})); );
console.error('[DEBUG] Integration Final Config - INCLUDE:', finalConfig.test?.include); console.error('[DEBUG] Integration Final Config - INCLUDE:', finalConfig.test?.include);
console.error('[DEBUG] Integration Final Config - EXCLUDE:', finalConfig.test?.exclude); console.error('[DEBUG] Integration Final Config - EXCLUDE:', finalConfig.test?.exclude);
console.error('[DEBUG] ----------------------------------\n'); console.error('[DEBUG] ----------------------------------\n');
export default finalConfig; export default finalConfig;

View File

@@ -7,8 +7,11 @@
* - Integration tests are defined in `vitest.config.integration.ts` and run in a 'node' environment. * - Integration tests are defined in `vitest.config.integration.ts` and run in a 'node' environment.
*/ */
export default [ export default [
// DEBUGGING LOG // DEBUGGING LOG
((): string => { console.error('\n[DEBUG] Loading vitest.workspace.ts'); return ''; })(), ((): string => {
'vite.config.ts', // Defines the 'unit' test project console.error('\n[DEBUG] Loading vitest.workspace.ts');
return '';
})(),
'vite.config.ts', // Defines the 'unit' test project
'vitest.config.integration.ts', // Defines the 'integration' test project 'vitest.config.integration.ts', // Defines the 'integration' test project
]; ];