Files
basil/CLAUDE.md
Paul R Kartchner c3e3d66fef
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
feat: add family-based multi-tenant access control
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>
2026-04-17 08:08:10 -06:00

16 KiB

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

# 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

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

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:

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:

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:

    # 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:

    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:

    # 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