Some checks failed
Basil CI/CD Pipeline / Code Linting (push) Successful in 3m18s
Basil CI/CD Pipeline / Web Tests (push) Successful in 3m31s
Basil CI/CD Pipeline / Security Scanning (push) Has been cancelled
Basil CI/CD Pipeline / API Tests (push) Failing after 3m56s
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 3m11s
Basil CI/CD Pipeline / Trigger Deployment (push) Has been cancelled
Basil CI/CD Pipeline / Build All Packages (push) Has been cancelled
Basil CI/CD Pipeline / E2E Tests (push) Has been cancelled
Basil CI/CD Pipeline / Build & Push Docker Images (push) Has been cancelled
Introduces Family as the tenant boundary so recipes and cookbooks can be scoped per household instead of every user seeing everything. Adds a centralized access filter, an invite/membership UI, a first-login prompt to create a family, and locks down the previously unauthenticated backup routes to admin only. - Family and FamilyMember models with OWNER/MEMBER roles; familyId on Recipe and Cookbook (ON DELETE SET NULL so deleting a family orphans content rather than destroying it). - access.service.ts composes a single WhereInput covering owner, family, PUBLIC visibility, and direct share; admins short-circuit to full access. - recipes/cookbooks routes now require auth, strip client-supplied userId/familyId on create, and gate mutations with canMutate checks. Auto-filter helpers scoped to the same family to prevent cross-tenant leakage via shared tag names. - families.routes.ts exposes list/create/get/rename/delete plus add/remove member, with last-owner protection on removal. - FamilyGate component blocks the authenticated UI with a modal if the user has zero memberships, prompting them to create their first family; Family page provides ongoing management. - backup.routes.ts now requires admin; it had no auth at all before. - Bumps version to 2026.04.008 and documents the monotonic PPP counter in CLAUDE.md. Migration SQL is generated locally but not tracked (per existing .gitignore); apply 20260416010000_add_family_tenant to prod during deploy. Run backfill-family-tenant.ts once post-migration to assign existing content to a default owner's family. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
424 lines
16 KiB
Markdown
424 lines
16 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
Basil is a full-stack recipe manager built with TypeScript. It features a REST API backend, React web frontend, and PostgreSQL database. The application supports importing recipes from URLs via schema.org markup parsing, local/S3 image storage, and comprehensive recipe management (CRUD operations, search, tagging).
|
|
|
|
## Architecture
|
|
|
|
**Monorepo Structure:**
|
|
- `packages/shared/` - Shared TypeScript types and interfaces used across all packages
|
|
- `packages/api/` - Express.js REST API server with Prisma ORM
|
|
- `packages/web/` - React + Vite web application
|
|
- Future: `packages/mobile/` - React Native mobile apps
|
|
|
|
**Key Technologies:**
|
|
- Backend: Node.js, TypeScript, Express, Prisma ORM, PostgreSQL
|
|
- Frontend: React, TypeScript, Vite, React Router
|
|
- Infrastructure: Docker, Docker Compose, nginx
|
|
- Recipe Scraping: Cheerio for HTML parsing, Schema.org Recipe markup support
|
|
|
|
**Database Schema:**
|
|
- `Recipe` - Main recipe entity with metadata (prep/cook time, servings, ratings)
|
|
- `Ingredient` - Recipe ingredients with amounts, units, and ordering
|
|
- `Instruction` - Step-by-step instructions with optional images
|
|
- `RecipeImage` - Multiple images per recipe
|
|
- `Tag` / `RecipeTag` - Many-to-many tagging system
|
|
|
|
**Storage Architecture:**
|
|
- Primary: Local filesystem storage (`./uploads`)
|
|
- Optional: S3 storage (configurable via environment variables)
|
|
- Storage service implements strategy pattern for easy switching
|
|
|
|
## Development Commands
|
|
|
|
```bash
|
|
# Install all dependencies (root + all packages)
|
|
npm install
|
|
|
|
# Development mode (starts all packages with hot reload)
|
|
npm run dev
|
|
|
|
# Build all packages
|
|
npm run build
|
|
|
|
# Lint all packages
|
|
npm run lint
|
|
|
|
# Testing
|
|
npm test # Run all unit tests
|
|
npm run test:e2e # Run E2E tests with Playwright
|
|
npm run test:e2e:ui # Run E2E tests with Playwright UI
|
|
|
|
# Docker commands
|
|
npm run docker:up # Start all services (PostgreSQL, API, web)
|
|
npm run docker:down # Stop all services
|
|
npm run docker:build # Rebuild Docker images
|
|
```
|
|
|
|
### Backend-Specific Commands
|
|
|
|
```bash
|
|
cd packages/api
|
|
|
|
# Development with auto-reload
|
|
npm run dev
|
|
|
|
# Database migrations
|
|
npm run prisma:migrate # Create and apply migration
|
|
npm run prisma:generate # Generate Prisma client
|
|
npm run prisma:studio # Open Prisma Studio GUI
|
|
|
|
# Build for production
|
|
npm run build
|
|
npm start
|
|
```
|
|
|
|
### Frontend-Specific Commands
|
|
|
|
```bash
|
|
cd packages/web
|
|
|
|
# Development server
|
|
npm run dev
|
|
|
|
# Production build
|
|
npm run build
|
|
npm run preview # Preview production build locally
|
|
```
|
|
|
|
## Configuration
|
|
|
|
**Environment Variables (packages/api/.env):**
|
|
```
|
|
PORT=3001
|
|
NODE_ENV=development
|
|
DATABASE_URL=postgresql://basil:basil@localhost:5432/basil?schema=public
|
|
STORAGE_TYPE=local # or 's3'
|
|
LOCAL_STORAGE_PATH=./uploads
|
|
BACKUP_PATH=./backups
|
|
CORS_ORIGIN=http://localhost:5173
|
|
```
|
|
|
|
For S3 storage, add:
|
|
```
|
|
S3_BUCKET=basil-recipes
|
|
S3_REGION=us-east-1
|
|
S3_ACCESS_KEY_ID=your-key
|
|
S3_SECRET_ACCESS_KEY=your-secret
|
|
```
|
|
|
|
For remote PostgreSQL database, update:
|
|
```
|
|
DATABASE_URL=postgresql://username:password@remote-host:5432/basil?schema=public
|
|
```
|
|
|
|
## Key Features
|
|
|
|
### Recipe Import from URL
|
|
- `POST /api/recipes/import` - Scrapes recipe from URL using schema.org markup
|
|
- Extracts: title, description, ingredients, instructions, times, images
|
|
- Handles JSON-LD structured data (primary) with fallback to manual parsing
|
|
- Downloads and stores images locally or on S3
|
|
|
|
### Recipe Management
|
|
- Full CRUD operations on recipes
|
|
- Pagination and search (by title, description)
|
|
- Filtering by cuisine, category
|
|
- Image upload with multiple images per recipe
|
|
- Tagging system for organization
|
|
|
|
### Storage Service
|
|
- Abstracted storage interface in `packages/api/src/services/storage.service.ts`
|
|
- Local storage: Saves to filesystem with timestamped filenames
|
|
- S3 storage: Placeholder for AWS SDK implementation
|
|
- Easy to extend for other storage providers
|
|
|
|
### Backup & Restore
|
|
- Complete data backup to single ZIP file including database and uploaded files
|
|
- Backup service in `packages/api/src/services/backup.service.ts`
|
|
- REST API for creating, listing, downloading, and restoring backups
|
|
- Automatic backup of all recipes, cookbooks, tags, and relationships
|
|
- Configurable backup storage location via `BACKUP_PATH` environment variable
|
|
|
|
## Adding New Features
|
|
|
|
### Adding a New API Endpoint
|
|
1. Add route handler in `packages/api/src/routes/*.routes.ts`
|
|
2. Update shared types in `packages/shared/src/types.ts` if needed
|
|
3. Rebuild shared package: `cd packages/shared && npm run build`
|
|
4. Use Prisma client for database operations
|
|
|
|
### Adding a New Frontend Page
|
|
1. Create component in `packages/web/src/pages/`
|
|
2. Add route in `packages/web/src/App.tsx`
|
|
3. Create API service methods in `packages/web/src/services/api.ts`
|
|
4. Import and use shared types from `@basil/shared`
|
|
|
|
### Database Schema Changes
|
|
1. Edit `packages/api/prisma/schema.prisma`
|
|
2. Run `npm run prisma:migrate` to create migration
|
|
3. Run `npm run prisma:generate` to update Prisma client
|
|
4. Update TypeScript types in `packages/shared/src/types.ts` to match
|
|
|
|
## Docker Deployment
|
|
|
|
The project includes full Docker support for production deployment:
|
|
|
|
```bash
|
|
docker-compose up -d
|
|
```
|
|
|
|
This starts:
|
|
- PostgreSQL database (port 5432)
|
|
- API server (port 3001)
|
|
- Web frontend served via nginx (port 5173)
|
|
|
|
Persistent volumes:
|
|
- `postgres_data` - Database storage
|
|
- `uploads_data` - Uploaded images
|
|
- `backups_data` - Backup files
|
|
|
|
### Using a Remote Database
|
|
|
|
To use a remote PostgreSQL database instead of the local Docker container:
|
|
|
|
1. Set the `DATABASE_URL` environment variable to point to your remote database
|
|
2. Update `docker-compose.yml` to pass the environment variable or create a `.env` file in the root
|
|
3. Optionally, remove or comment out the `postgres` service and its dependency in `docker-compose.yml`
|
|
|
|
Example `.env` file in project root:
|
|
```
|
|
DATABASE_URL=postgresql://username:password@remote-host:5432/basil?schema=public
|
|
```
|
|
|
|
The docker-compose.yml is configured to use `${DATABASE_URL:-default}` which will use the environment variable if set, or fall back to the local postgres container.
|
|
|
|
## API Reference
|
|
|
|
**Recipes:**
|
|
- `GET /api/recipes` - List all recipes (supports pagination, search, filters)
|
|
- `GET /api/recipes/:id` - Get single recipe with all relations
|
|
- `POST /api/recipes` - Create new recipe
|
|
- `PUT /api/recipes/:id` - Update recipe
|
|
- `DELETE /api/recipes/:id` - Delete recipe and associated images
|
|
- `POST /api/recipes/:id/images` - Upload image for recipe
|
|
- `POST /api/recipes/import` - Import recipe from URL
|
|
|
|
**Query Parameters:**
|
|
- `page`, `limit` - Pagination
|
|
- `search` - Search in title/description
|
|
- `cuisine`, `category` - Filter by cuisine or category
|
|
|
|
**Backups:**
|
|
- `POST /api/backup` - Create a new backup (returns backup metadata)
|
|
- `GET /api/backup` - List all available backups
|
|
- `GET /api/backup/:filename` - Download a specific backup file
|
|
- `POST /api/backup/restore` - Restore from backup (accepts file upload or existing filename)
|
|
- `DELETE /api/backup/:filename` - Delete a backup file
|
|
|
|
## Important Implementation Details
|
|
|
|
### Prisma Relations
|
|
- All related entities (ingredients, instructions, images, tags) use cascade delete
|
|
- Ingredients and instructions maintain ordering via `order` and `step` fields
|
|
- Tags use many-to-many relationship via `RecipeTag` join table
|
|
|
|
### Recipe Scraping
|
|
- Primary: Parses JSON-LD `application/ld+json` scripts for Schema.org Recipe data
|
|
- Fallback: Extracts basic info from HTML meta tags and headings
|
|
- Handles ISO 8601 duration format (PT30M, PT1H30M) for cook times
|
|
- Downloads images asynchronously and stores them locally
|
|
|
|
### Frontend Routing
|
|
- Uses React Router v6 for client-side routing
|
|
- Vite proxy forwards `/api` and `/uploads` requests to backend during development
|
|
- Production uses nginx reverse proxy to backend API
|
|
|
|
### TypeScript Workspace
|
|
- Root `package.json` defines npm workspaces
|
|
- Packages can reference each other (e.g., `@basil/shared`)
|
|
- Must rebuild shared package when types change for other packages to see updates
|
|
|
|
## CI/CD and Deployment
|
|
|
|
Basil includes a complete CI/CD pipeline with Gitea Actions for automated testing, building, and deployment.
|
|
|
|
**Quick Start:**
|
|
- See [CI/CD Setup Guide](docs/CI-CD-SETUP.md) for full documentation
|
|
- See [Deployment Quick Start](docs/DEPLOYMENT-QUICK-START.md) for quick reference
|
|
|
|
**Pipeline Overview:**
|
|
1. **Test Stage**: Runs unit tests (Vitest) and E2E tests (Playwright)
|
|
2. **Build Stage**: Builds Docker images for API and Web (main branch only)
|
|
3. **Deploy Stage**: Pushes images to registry and triggers webhook deployment
|
|
|
|
**Deployment Options:**
|
|
- **Automatic**: Push to main branch triggers full CI/CD pipeline
|
|
- **Manual**: Run `./scripts/manual-deploy.sh` for interactive deployment
|
|
- **Webhook**: Systemd service listens for deployment triggers
|
|
|
|
**Key Files:**
|
|
- `.gitea/workflows/ci-cd.yml` - Main CI/CD workflow
|
|
- `scripts/deploy.sh` - Deployment script
|
|
- `scripts/webhook-receiver.sh` - Webhook server
|
|
- `.env.deploy.example` - Deployment configuration template
|
|
|
|
**Required Secrets (Gitea):**
|
|
- `DOCKER_USERNAME` - Docker Hub username
|
|
- `DOCKER_PASSWORD` - Docker Hub access token
|
|
- `DEPLOY_WEBHOOK_URL` - Webhook endpoint for deployments
|
|
|
|
## Version Management
|
|
|
|
**IMPORTANT**: Increment the version with **every production deployment**.
|
|
|
|
### Version Format
|
|
Basil uses calendar versioning with the format: `YYYY.MM.PPP`
|
|
- `YYYY` - Four-digit year (e.g., 2026)
|
|
- `MM` - Two-digit month with zero-padding (e.g., 01 for January, 12 for December)
|
|
- `PPP` - Three-digit patch number with zero-padding that increases with every deployment. **Does not reset at month boundaries** — it is a monotonically increasing counter across the lifetime of the project.
|
|
|
|
### Examples
|
|
- `2026.01.006` - Sixth deployment (in January 2026)
|
|
- `2026.04.007` - Seventh deployment (in April 2026 — patch continues from previous month, does not reset)
|
|
- `2026.04.008` - Eighth deployment (still in April 2026)
|
|
- `2026.05.009` - Ninth deployment (in May 2026 — patch continues, does not reset)
|
|
|
|
### Version Update Process
|
|
When deploying to production:
|
|
|
|
1. **Update version files:**
|
|
```bash
|
|
# Update both version files with new version
|
|
# packages/api/src/version.ts
|
|
# packages/web/src/version.ts
|
|
export const APP_VERSION = '2026.01.002';
|
|
```
|
|
|
|
2. **Commit the version bump:**
|
|
```bash
|
|
git add packages/api/src/version.ts packages/web/src/version.ts
|
|
git commit -m "chore: bump version to 2026.01.002"
|
|
git push origin main
|
|
```
|
|
|
|
3. **Create Git tag and release:**
|
|
```bash
|
|
# Tag should match version with 'v' prefix
|
|
git tag v2026.01.002
|
|
git push origin v2026.01.002
|
|
|
|
# Or use Gitea MCP to create tag and release
|
|
```
|
|
|
|
4. **Document in release notes:**
|
|
- Summarize changes since last version
|
|
- List bug fixes, features, and breaking changes
|
|
- Reference related pull requests or issues
|
|
|
|
### Version Display
|
|
The current version is displayed in:
|
|
- API: `GET /api/version` endpoint returns `{ version: '2026.01.002' }`
|
|
- Web: Footer or about section shows current version
|
|
- Both packages export `APP_VERSION` constant for internal use
|
|
|
|
## UI Design System - Thumbnail Cards
|
|
|
|
### Responsive Column Layout System
|
|
|
|
All recipe and cookbook thumbnail displays support a responsive column system (3, 5, 7, or 9 columns) with column-specific styling for optimal readability at different densities.
|
|
|
|
**Column-Responsive Font Sizes:**
|
|
- **Column 3** (Largest cards): Title 0.95rem, Description 0.8rem (2 lines), Meta 0.75rem
|
|
- **Column 5** (Medium cards): Title 0.85rem, Description 0.75rem (2 lines), Meta 0.7rem
|
|
- **Column 7** (Compact): Title 0.75rem, Description hidden, Meta 0.6rem
|
|
- **Column 9** (Most compact): Title 0.75rem, Description hidden, Meta 0.6rem
|
|
|
|
**Implementation Pattern:**
|
|
1. Add `gridClassName = \`recipes-grid columns-${columnCount}\`` or `\`cookbooks-grid columns-${columnCount}\``
|
|
2. Apply className to grid container: `<div className={gridClassName} style={gridStyle}>`
|
|
3. Use column-specific CSS selectors: `.columns-3 .recipe-info h3 { font-size: 0.95rem; }`
|
|
|
|
### Recipe Thumbnail Display Locations
|
|
|
|
All locations use square aspect ratio (1:1) cards with 60% image height.
|
|
|
|
1. **Recipe List Page** (`packages/web/src/pages/RecipeList.tsx`)
|
|
- Class: `recipe-grid-enhanced columns-{3|5|7|9}`
|
|
- CSS: `packages/web/src/styles/RecipeList.css`
|
|
- Features: Main recipe browsing with pagination, search, filtering
|
|
- Displays: Image, title, description, time, rating
|
|
- Status: ✅ Responsive column styling applied
|
|
|
|
2. **Cookbooks Page - Recent Recipes** (`packages/web/src/pages/Cookbooks.tsx`)
|
|
- Class: `recipes-grid columns-{3|5|7|9}`
|
|
- CSS: `packages/web/src/styles/Cookbooks.css`
|
|
- Features: Shows 6 most recent recipes below cookbook list
|
|
- Displays: Image, title, description, time, rating
|
|
- Status: ✅ Responsive column styling applied
|
|
|
|
3. **Cookbook Detail - Recipes Section** (`packages/web/src/pages/CookbookDetail.tsx`)
|
|
- Class: `recipes-grid columns-{3|5|7|9}`
|
|
- CSS: `packages/web/src/styles/CookbookDetail.css`
|
|
- Features: Paginated recipes within a cookbook, with remove button
|
|
- Displays: Image, title, description, time, rating, remove button
|
|
- Status: ✅ Responsive column styling applied
|
|
|
|
4. **Add Meal Modal - Recipe Selection** (`packages/web/src/components/meal-planner/AddMealModal.tsx`)
|
|
- Class: `recipe-list` with `recipe-item`
|
|
- CSS: `packages/web/src/styles/AddMealModal.css`
|
|
- Features: Selectable recipe list for adding to meal plan
|
|
- Displays: Small thumbnail, title, description
|
|
- Status: ⚠️ Needs responsive column styling review
|
|
|
|
5. **Meal Card Component** (`packages/web/src/components/meal-planner/MealCard.tsx`)
|
|
- Class: `meal-card` with `meal-card-image`
|
|
- CSS: `packages/web/src/styles/MealCard.css`
|
|
- Features: Recipe thumbnail in meal planner (compact & full views)
|
|
- Displays: Recipe image as part of meal display
|
|
- Status: ⚠️ Different use case - calendar/list view, not grid-based
|
|
|
|
### Cookbook Thumbnail Display Locations
|
|
|
|
All locations use square aspect ratio (1:1) cards with 50% image height.
|
|
|
|
1. **Cookbooks Page - Main Grid** (`packages/web/src/pages/Cookbooks.tsx`)
|
|
- Class: `cookbooks-grid`
|
|
- CSS: `packages/web/src/styles/Cookbooks.css`
|
|
- Features: Main cookbook browsing with pagination
|
|
- Displays: Cover image, name, recipe count, cookbook count
|
|
- Status: ✅ Already has compact styling (description/tags hidden)
|
|
- Note: Could benefit from column-responsive font sizes
|
|
|
|
2. **Cookbook Detail - Nested Cookbooks** (`packages/web/src/pages/CookbookDetail.tsx`)
|
|
- Class: `cookbooks-grid` with `cookbook-card nested`
|
|
- CSS: `packages/web/src/styles/CookbookDetail.css`
|
|
- Features: Child cookbooks within parent cookbook
|
|
- Displays: Cover image, name, recipe count, cookbook count
|
|
- Status: ✅ Already has compact styling (description/tags hidden)
|
|
- Note: Could benefit from column-responsive font sizes
|
|
|
|
### Key CSS Classes
|
|
|
|
- `recipe-card` - Individual recipe card
|
|
- `recipe-grid-enhanced` or `recipes-grid` - Recipe grid container
|
|
- `cookbook-card` - Individual cookbook card
|
|
- `cookbooks-grid` - Cookbook grid container
|
|
- `columns-{3|5|7|9}` - Dynamic column count modifier class
|
|
|
|
### Styling Consistency Rules
|
|
|
|
1. **Image Heights**: Recipes 60%, Cookbooks 50%
|
|
2. **Aspect Ratio**: All cards are square (1:1)
|
|
3. **Border**: 1px solid #e0e0e0 (not box-shadow)
|
|
4. **Border Radius**: 8px
|
|
5. **Hover Effect**: `translateY(-2px)` with `box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1)`
|
|
6. **Description Display**:
|
|
- Columns 3 & 5: Show 2 lines
|
|
- Columns 7 & 9: Hide completely
|
|
7. **Font Scaling**: Larger fonts for fewer columns, smaller for more columns
|
|
8. **Text Truncation**: Use `-webkit-line-clamp` with `text-overflow: ellipsis`
|