costco-grocery-list/.github/copilot-instructions.md
2026-01-27 00:03:58 -08:00

14 KiB

Costco Grocery List - AI Agent Instructions

Architecture Overview

This is a full-stack grocery list management app with role-based access control (RBAC):

  • Backend: Node.js + Express + PostgreSQL (port 5000)
  • Frontend: React 19 + TypeScript + Vite (port 3000/5173)
  • Deployment: Docker Compose with separate dev/prod configurations

Mobile-First Design Principles

CRITICAL: All UI components MUST be designed for both mobile and desktop from the start.

Responsive Design Requirements:

  • Use relative units (rem, em, %, vh/vw) over fixed pixels where possible
  • Implement mobile breakpoints: 480px, 768px, 1024px
  • Test layouts at: 320px (small phone), 375px (phone), 768px (tablet), 1024px+ (desktop)
  • Avoid horizontal scrolling on mobile devices
  • Touch targets minimum 44x44px for mobile usability
  • Use max-width with margin: 0 auto for content containers
  • Stack elements vertically on mobile, use flexbox/grid for larger screens
  • Hide/collapse navigation into hamburger menus on mobile
  • Ensure modals/dropdowns work well on small screens

Common Patterns:

/* Mobile-first approach */
.container {
  padding: 1rem;
  max-width: 100%;
}

@media (min-width: 768px) {
  .container {
    padding: 2rem;
    max-width: 800px;
    margin: 0 auto;
  }
}

Key Design Patterns

Dual RBAC System - Two separate role hierarchies:

1. System Roles (users.role column):

  • system_admin: Access to Admin Panel for system-wide management (stores, users)
  • user: Regular system user (default for new registrations)
  • Defined in backend/models/user.model.js
  • Used for Admin Panel access control

2. Household Roles (household_members.role column):

  • admin: Can manage household members, change roles, delete household
  • user: Can add/edit items, mark as bought (standard member permissions)
  • Defined per household membership
  • Used for household-level permissions (item management, member management)

Important: Always distinguish between system role and household role:

  • System role: From AuthContext or req.user.role - controls Admin Panel access
  • Household role: From activeHousehold.role or household_members.role - controls household operations

Middleware chain pattern for protected routes:

// System-level protection
router.get("/stores", auth, requireRole("system_admin"), controller.getAllStores);

// Household-level checks done in controller
router.post("/lists/:householdId/items", auth, controller.addItem);
  • auth middleware extracts JWT from Authorization: Bearer <token> header
  • requireRole checks system role only
  • Household role checks happen in controllers using household.model.js methods

Frontend route protection:

  • <PrivateRoute>: Requires authentication, redirects to /login if no token
  • <RoleGuard allowed={[ROLES.SYSTEM_ADMIN]}>: Requires system_admin role for Admin Panel
  • Household permissions: Check activeHousehold.role in components (not route-level)
  • Example in frontend/src/App.jsx

Multi-Household Architecture:

  • Users can belong to multiple households
  • Each household has its own grocery lists, stores, and item classifications
  • HouseholdContext manages active household selection
  • All list operations are scoped to the active household

Database Schema

PostgreSQL server runs externally - not in Docker Compose. Connection configured in backend/.env via standard environment variables.

Core Tables:

users - System users

  • id (PK), username, password (bcrypt), name, display_name
  • role: system_admin | user (default: viewer - legacy)
  • System-level authentication and authorization

households - Household entities

  • id (PK), name, invite_code, created_by, created_at
  • Each household is independent with own lists and members

household_members - Junction table (users ↔ households)

  • id (PK), household_id (FK), user_id (FK), role, joined_at
  • role: admin | user (household-level permissions)
  • One user can belong to multiple households with different roles

items - Master item catalog

  • id (PK), name, default_image, default_image_mime_type, usage_count
  • Shared across all households, case-insensitive unique names

stores - Store definitions (system-wide)

  • id (PK), name, default_zones (JSONB array)
  • Managed by system_admin in Admin Panel

household_stores - Stores available to each household

  • id (PK), household_id (FK), store_id (FK), is_default
  • Links households to stores they use

household_lists - Grocery list items per household

  • id (PK), household_id (FK), store_id (FK), item_id (FK)
  • quantity, bought, custom_image, custom_image_mime_type
  • added_by, modified_on
  • Scoped to household + store combination

household_list_history - Tracks quantity contributions

  • id (PK), household_list_id (FK), quantity, added_by, added_on
  • Multi-contributor tracking (who added how much)

household_item_classifications - Item classifications per household/store

  • id (PK), household_id, store_id, item_id
  • item_type, item_group, zone, confidence, source
  • Household-specific overrides of global classifications

item_classification - Global item classifications

  • id (PK), item_type, item_group, zone, confidence, source
  • System-wide defaults for item categorization

Legacy Tables (deprecated, may still exist):

  • grocery_list, grocery_history - Old single-household implementation

Important patterns:

  • No formal migration system - schema changes are manual SQL
  • Items use case-insensitive matching (ILIKE) to prevent duplicates
  • JOINs with ARRAY_AGG for multi-contributor queries (see backend/models/list.model.v2.js)
  • All list operations require household_id parameter for scoping
  • Image storage: bytea columns for images with separate MIME type columns

Development Workflow

Local Development

# Start all services with hot-reload against LOCAL database
docker-compose -f docker-compose.dev.yml up

# Backend runs nodemon (watches backend/*.js)
# Frontend runs Vite dev server with HMR on port 3000

Key dev setup details:

  • Volume mounts preserve node_modules in containers while syncing source code
  • Backend uses Dockerfile (standard) with npm run dev override
  • Frontend uses Dockerfile.dev with CHOKIDAR_USEPOLLING=true for file watching
  • Both connect to external PostgreSQL server (configured in backend/.env)
  • No database container in compose - DB is managed separately

Production Build

# Local production build (for testing)
docker-compose -f docker-compose.prod.yml up --build

# Actual production uses pre-built images
docker-compose up  # Pulls from private registry

CI/CD Pipeline (Gitea Actions)

See .gitea/workflows/deploy.yml for full workflow:

Build stage (on push to main):

  1. Run backend tests (npm test --if-present)
  2. Build backend image with tags: :latest and :<commit-sha>
  3. Build frontend image with tags: :latest and :<commit-sha>
  4. Push both images to private registry

Deploy stage:

  1. SSH to production server
  2. Upload docker-compose.yml to deployment directory
  3. Pull latest images and restart containers with docker compose up -d
  4. Prune old images

Notify stage:

  • Sends deployment status via webhook

Required secrets:

  • REGISTRY_USER, REGISTRY_PASS: Docker registry credentials
  • DEPLOY_HOST, DEPLOY_USER, DEPLOY_KEY: SSH deployment credentials

Backend Scripts

  • npm run dev: Start with nodemon
  • npm run build: esbuild compilation + copy public assets to dist/
  • npm test: Run Jest tests (currently no tests exist)

Frontend Scripts

  • npm run dev: Vite dev server (port 5173)
  • npm run build: TypeScript compilation + Vite production build

Docker Configurations

docker-compose.yml (production):

  • Pulls pre-built images from private registry
  • Backend on port 5000, frontend on port 3000 (nginx serves on port 80)
  • Requires backend.env and frontend.env files

docker-compose.dev.yml (local development):

  • Builds images locally from Dockerfile/Dockerfile.dev
  • Volume mounts for hot-reload: ./backend:/app and ./frontend:/app
  • Named volumes preserve node_modules between rebuilds
  • Backend uses backend/.env directly
  • Frontend uses Dockerfile.dev with polling enabled for cross-platform compatibility

docker-compose.prod.yml (local production testing):

  • Builds images locally using production Dockerfiles
  • Backend: Standard Node.js server
  • Frontend: Multi-stage build with nginx serving static files

Configuration & Environment

Backend (backend/.env):

  • Database connection variables (host, user, password, database name)
  • JWT_SECRET: Token signing key
  • ALLOWED_ORIGINS: Comma-separated CORS whitelist (supports static origins + 192.168.*.* IP ranges)
  • PORT: Server port (default 5000)

Frontend (environment variables):

  • VITE_API_URL: Backend base URL

Config accessed via:

Authentication Flow

  1. User logs in → backend returns {token, userId, role, username} (backend/controllers/auth.controller.js)
    • role is the system role (system_admin or user)
  2. Frontend stores in localStorage and AuthContext (frontend/src/context/AuthContext.jsx)
  3. HouseholdContext loads user's households and sets active household
    • Active household includes household.role (the household role)
  4. Axios interceptor auto-attaches Authorization: Bearer <token> header (frontend/src/api/axios.js)
  5. Backend validates JWT on protected routes (backend/middleware/auth.js)
    • Sets req.user = { id, role, username } with system role
  6. Controllers check household membership/role using backend/models/household.model.js
  7. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login

Critical Conventions

Security Practices

  • Never expose credentials: Do not hardcode or document actual values for JWT_SECRET, database passwords, API keys, or any sensitive configuration
  • No infrastructure details: Avoid documenting specific IP addresses, domain names, deployment paths, or server locations in code or documentation
  • Environment variables: Reference .env files conceptually - never include actual contents
  • Secrets in CI/CD: Document that secrets are required, not their values
  • Code review: Scan all changes for accidentally committed credentials before pushing

Backend

Frontend

  • Mixed JSX/TSX: Some components are .jsx (JavaScript), others .tsx (TypeScript) - maintain existing file extensions
  • API calls: Use centralized api instance from frontend/src/api/axios.js, not raw axios
  • Role checks: Access role from AuthContext, compare with constants from frontend/src/constants/roles.js
  • Navigation: Use React Router's <Navigate> for redirects, not window.location (except in interceptor)

Common Tasks

Add a new protected route:

  1. Backend: Add route with auth middleware (+ requireRole(...) if system role check needed)
  2. Frontend: Add route in frontend/src/App.jsx wrapped in <PrivateRoute> (and <RoleGuard> for Admin Panel)

Access user info in backend controller:

const { id, role } = req.user; // Set by auth middleware (system role)
const userId = req.user.id;

Check household permissions in backend controller:

const householdRole = await household.getUserRole(householdId, userId);
if (!householdRole) return res.status(403).json({ message: "Not a member of this household" });
if (householdRole !== 'admin') return res.status(403).json({ message: "Household admin required" });

Check household permissions in frontend:

const { activeHousehold } = useContext(HouseholdContext);
const householdRole = activeHousehold?.role; // 'admin' or 'user'

// Allow all members except viewers (no viewer role in households)
const canManageItems = householdRole && householdRole !== 'viewer'; // Usually just check if role exists

// Admin-only actions
const canManageMembers = householdRole === 'admin';

Query grocery items with contributors: Use the JOIN pattern in backend/models/list.model.v2.js - aggregates user names via household_list_history table.

Testing

Backend:

  • Jest configured at root level (package.json)
  • Currently no test files exist - testing infrastructure needs development
  • CI/CD runs npm test --if-present but will pass if no tests found
  • Focus area: API endpoint testing (use supertest with Express)

Frontend:

To add backend tests:

  1. Create backend/__tests__/ directory
  2. Use Jest + Supertest pattern for API tests
  3. Mock database calls or use test database