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

@@ -133,22 +133,24 @@ 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)" 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) (1 row)
## production postgres setup ## production postgres setup
Part 1: Production Database Setup Part 1: Production Database 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 ...
@@ -308,7 +312,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
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
@@ -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,13 +350,10 @@ $ 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:
@@ -366,6 +367,7 @@ Initialize Podman: Launch Podman Desktop. It will automatically set up its WSL 2
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: - Pull Ubuntu Image: Open a PowerShell or command prompt and pull the latest Ubuntu image:
podman pull ubuntu:latest podman pull ubuntu:latest
- Create a Podman Volume: Create a volume to persist node_modules and avoid installing them every time the container starts. - Create a Podman Volume: Create a volume to persist node_modules and avoid installing them every time the container starts.
@@ -402,27 +404,21 @@ podman run -it -p 3001:3001 -p 5173:5173 --name flyer-dev -v "D:\gitea\flyer-cra
npm run dev npm run dev
6. Accessing the Application 6. Accessing the Application
- Frontend: Open your browser and go to http://localhost:5173. - Frontend: Open your browser and go to http://localhost:5173.
- Backend: The frontend will make API calls to http://localhost:3001. - 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. - Stopping the Container: Press Ctrl+C in the container terminal, then type exit.
- Restarting the Container: - Restarting the Container:
podman start -a -i flyer-dev 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

@@ -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,10 +24,7 @@ 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

2
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';
/** /**

View File

@@ -1,8 +1,8 @@
<!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');

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,7 +10,10 @@ 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: {

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,7 +8,7 @@ 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);
@@ -19,6 +19,5 @@ root.render(
<App /> <App />
</AppProviders> </AppProviders>
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>,
); );

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,7 +166,6 @@ 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.
@@ -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',
]; ];
/** /**

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,9 +17,7 @@
"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.

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

@@ -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,7 +60,9 @@ 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',

View File

@@ -30,7 +30,9 @@ 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(
baseViteConfig,
defineConfig({
test: { test: {
// Override settings from the main config for this specific test project. // Override settings from the main config for this specific test project.
name: 'integration', name: 'integration',
@@ -57,8 +59,9 @@ const finalConfig = mergeConfig(baseViteConfig, defineConfig({
reportOnFailure: true, // This ensures the report generates even if tests fail reportOnFailure: true, // This ensures the report generates even if tests fail
clean: true, 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);

View File

@@ -8,7 +8,10 @@
*/ */
export default [ export default [
// DEBUGGING LOG // DEBUGGING LOG
((): string => { console.error('\n[DEBUG] Loading vitest.workspace.ts'); return ''; })(), ((): string => {
console.error('\n[DEBUG] Loading vitest.workspace.ts');
return '';
})(),
'vite.config.ts', // Defines the 'unit' test project '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
]; ];