diff --git a/AGENTS.md b/AGENTS.md index d2976e5..b85af73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ packages/shared/ zod schemas + TS DTOs mirroring DB (canonical client contract docs/ ARCHITECTURE.md, ERD.md, DFD.md, planning/ tools/ composer.phar (vendored, no global composer) DECISIONS.md locked stack + flows (source of truth) +compose.yaml optional Docker Desktop local-dev stack (not production) ``` Three client mirrors must stay in sync on any schema/enum change: `db/schema.sql`, `packages/shared/src/index.ts`, `apps/web/src/types.ts`. @@ -58,6 +59,9 @@ bunx cap open android # build APK in Android Studio # Root shortcuts bun run dev # runs web dev server bun run build:mobile # web build + cap sync + +# Optional Docker local stack (repo root) +docker compose up --build # web :5173, api :8000, mailpit :8025, mysql :3307 ``` ## 2a. Known-good production deployment contract @@ -88,6 +92,9 @@ free-tier deployment contract: `http://10.0.2.2` for emulator / compatibility APK testing - Mobile: `bun run build:mobile` builds against the deployed Render API; `bun run build:mobile:local` is only for local emulator API testing +- Local Docker: `compose.yaml` is optional dev tooling only. It may use + MySQL/Mailpit/Tesseract locally, but it does not replace Render, Vercel, + TiDB, or Android Studio production/test responsibilities. Never store TiDB, Cloudinary, Brevo, APP_KEY, or SMTP secrets in docs, memory, or committed files. diff --git a/DECISIONS.md b/DECISIONS.md index 9bc830c..0cdde11 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -13,6 +13,7 @@ | Image storage | Cloudinary | Payment proofs + menu photos; signed uploads from PHP | | Auth | PHP sessions, `password_hash`, `session_regenerate_id`, idle timeout, role in session | Laravel session driver + CSRF | | Hosting | API: Render free Docker web service; DB: TiDB Cloud Starter; SPA: Vercel static app; Mobile: Capacitor APK | XAMPP for local dev; provider subdomains are the free domains | +| Optional local dev | Docker Compose | Local API + MySQL + Mailpit + Tesseract + Vite parity stack; does not replace production providers | ## 2. Why this stack (rubric alignment) @@ -220,6 +221,11 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready - Because Vercel and Render are different sites, production session cookies default to `SameSite=None; Secure`. Local development stays `SameSite=Lax` unless `.env` overrides it. +- Docker Compose is allowed as an optional local parity layer. It exists to + make Laravel/MySQL/Tesseract/Mailpit development reproducible on machines + with Docker Desktop. It must not replace the known-good production contract: + Vercel for web, Render for API, TiDB Cloud for production data, and direct + Render API targeting for production APK builds. ### 8j. Customer account completion and pickup-aware UX - Payment proofs are digital receipts, so customer receipt upload must open the diff --git a/README.md b/README.md index 7e32925..901490b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Admin Dashboard | API Health | Deployment Guide | + Docker Local Dev | Final Report | Ops Session Log

@@ -71,6 +72,7 @@ Use these dashboards when something breaks in production. Do not put secrets in | Cloudinary | PrepFlow image cloud | Menu photos, payment proofs, payment-method QR images, OCR input URLs | API stores Cloudinary URLs only; no data URLs in DB | | Brevo | Transactional email | SMTP credentials, sender verification, delivery/open logs | Render uses SMTP on port `2525`; domain auth still needs an owned domain | | Android Studio | Local `apps/mobile/android` project | Debug APK build/install/logcat | `bun run build:mobile` targets deployed Render API for production APK testing | +| Docker Desktop | Local `compose.yaml` | Optional repeatable dev stack for API, MySQL, Mailpit, Vite, Tesseract | Use for local parity; not a replacement for Vercel/Render/TiDB dashboards | Provider dashboard details and recovery steps live in [docs/deployment/free-tier.md](docs/deployment/free-tier.md). The June 28 deployment/auth hardening log is in [docs/planning/deployment-hardening-session-2026-06-28.md](docs/planning/deployment-hardening-session-2026-06-28.md). @@ -198,15 +200,40 @@ PrepFlow/ ## Local Development +Two local paths are supported: + +- **XAMPP path:** closest to the original classroom setup and still fully + supported. +- **Docker path:** optional, repeatable local stack for Laravel + MySQL + + Mailpit + Tesseract + Vite. This is useful for avoiding XAMPP drift and + matching Render's Linux/Tesseract runtime more closely. + ### Prerequisites - Windows + PowerShell - XAMPP with PHP 8.2 and MySQL/MariaDB (`C:\xampp\php` expected) - Bun 1.3+ +- Docker Desktop, optional but recommended for backend/OCR/email parity - Android Studio + Android SDK for APK work - Cloudinary account for image upload testing - Optional local OCR: Tesseract at `C:\Program Files\Tesseract-OCR\tesseract.exe` +### Docker Quick Start + +```powershell +cd D:\OneDrive\Desktop\Final-Project\PrepFlow +docker compose up --build +``` + +Open: + +- Web: http://localhost:5173 +- API health: http://localhost:8000/api/menu +- Mailpit email inbox: http://localhost:8025 + +Docker uses local dev credentials only and runs migrations + seeders on start. +Full details: [docs/development/docker-local.md](docs/development/docker-local.md). + ### 1. Install Workspace Dependencies ```powershell @@ -387,6 +414,7 @@ Read [AGENTS.md](AGENTS.md) and [DECISIONS.md](DECISIONS.md) before editing impl | [docs/ERD.md](docs/ERD.md) | Entity relationship model | | [docs/DFD.md](docs/DFD.md) | Data flow diagrams | | [docs/deployment/free-tier.md](docs/deployment/free-tier.md) | Render + TiDB + Vercel deployment guide | +| [docs/development/docker-local.md](docs/development/docker-local.md) | Optional Docker Compose local development guide | | [docs/planning/session-final-report.md](docs/planning/session-final-report.md) | Implementation and verification report | | [docs/planning/deployment-hardening-session-2026-06-28.md](docs/planning/deployment-hardening-session-2026-06-28.md) | Deployment/auth/email/OCR/APK hardening session log | | [docs/planning/completion-audit.md](docs/planning/completion-audit.md) | Historical audit baseline | diff --git a/api/Dockerfile.dev b/api/Dockerfile.dev new file mode 100644 index 0000000..0e65dc9 --- /dev/null +++ b/api/Dockerfile.dev @@ -0,0 +1,35 @@ +FROM php:8.2-apache + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + default-mysql-client \ + git \ + libsqlite3-dev \ + tesseract-ocr \ + unzip \ + zip \ + && docker-php-ext-install pdo_mysql pdo_sqlite \ + && a2enmod headers rewrite \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /var/www/html + +COPY tools/composer.phar /tmp/composer.phar +COPY api/ ./ +COPY api/docker/apache.conf /etc/apache2/sites-available/000-default.conf +COPY api/docker/dev-start.sh /usr/local/bin/prepflow-dev-start +COPY api/docker/test.sh /usr/local/bin/prepflow-test + +RUN mkdir -p bootstrap/cache storage/logs storage/framework/cache storage/framework/sessions storage/framework/views \ + && chmod -R ug+rwx bootstrap/cache storage \ + && chmod +x /usr/local/bin/prepflow-dev-start /usr/local/bin/prepflow-test \ + && chown -R www-data:www-data storage bootstrap/cache + +ENV OCR_TECHNIQUE=tesseract +ENV TESSERACT_BIN=/usr/bin/tesseract + +EXPOSE 80 + +CMD ["prepflow-dev-start"] diff --git a/api/database/migrations/2025_06_27_000013_create_daily_procurement_view.php b/api/database/migrations/2025_06_27_000013_create_daily_procurement_view.php index e2f7624..c3702d9 100644 --- a/api/database/migrations/2025_06_27_000013_create_daily_procurement_view.php +++ b/api/database/migrations/2025_06_27_000013_create_daily_procurement_view.php @@ -7,6 +7,8 @@ { public function up(): void { + DB::statement('DROP VIEW IF EXISTS v_daily_procurement'); + DB::statement( "CREATE VIEW v_daily_procurement AS SELECT ri.ingredient_name, diff --git a/api/docker/dev-start.sh b/api/docker/dev-start.sh new file mode 100644 index 0000000..9adb856 --- /dev/null +++ b/api/docker/dev-start.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +mkdir -p storage/logs storage/framework/cache storage/framework/sessions storage/framework/views bootstrap/cache +chmod -R ug+rwx storage bootstrap/cache + +if [ -z "${APP_KEY:-}" ]; then + export APP_KEY + APP_KEY="$(php -r 'echo "base64:".base64_encode(random_bytes(32));')" +fi + +if [ ! -f vendor/autoload.php ]; then + php /tmp/composer.phar install --no-interaction --prefer-dist || php /tmp/composer.phar install --no-interaction --prefer-source +fi + +if [ "${DB_CONNECTION:-}" = "mysql" ]; then + until MYSQL_PWD="${DB_PASSWORD:-}" mysqladmin ping -h"${DB_HOST:-mysql}" -P"${DB_PORT:-3306}" -u"${DB_USERNAME:-root}" --silent; do + echo "Waiting for MySQL..." + sleep 2 + done +fi + +if [ "${RUN_MIGRATIONS:-false}" = "true" ]; then + php artisan migrate --force +fi + +if [ "${RUN_SEEDERS:-false}" = "true" ]; then + php artisan db:seed --force +fi + +php artisan config:clear --no-interaction +php artisan route:clear --no-interaction +php artisan view:clear --no-interaction + +chown -R www-data:www-data storage bootstrap/cache + +exec apache2-foreground diff --git a/api/docker/test.sh b/api/docker/test.sh new file mode 100644 index 0000000..bf5ac44 --- /dev/null +++ b/api/docker/test.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_ENV=testing \ +APP_MAINTENANCE_DRIVER=file \ +BCRYPT_ROUNDS=4 \ +BROADCAST_CONNECTION=null \ +CACHE_STORE=array \ +DB_CONNECTION=sqlite \ +DB_DATABASE=':memory:' \ +DB_URL='' \ +MAIL_MAILER=array \ +QUEUE_CONNECTION=sync \ +SESSION_DRIVER=array \ +PULSE_ENABLED=false \ +TELESCOPE_ENABLED=false \ +NIGHTWATCH_ENABLED=false \ +php artisan test "$@" diff --git a/apps/web/vite.config.js b/apps/web/vite.config.js index dbaec3c..3720c1f 100644 --- a/apps/web/vite.config.js +++ b/apps/web/vite.config.js @@ -10,7 +10,7 @@ export default defineConfig({ server: { port: 5173, proxy: { - '/api': 'http://127.0.0.1:8000', + '/api': process.env.VITE_DEV_API_PROXY || 'http://127.0.0.1:8000', }, }, build: { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index d6accfe..93a222a 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ server: { port: 5173, proxy: { - '/api': 'http://127.0.0.1:8000', + '/api': process.env.VITE_DEV_API_PROXY || 'http://127.0.0.1:8000', }, }, build: { diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..75704c8 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,113 @@ +name: prepflow-dev + +services: + mysql: + image: mysql:8.4 + environment: + MYSQL_DATABASE: prepflow + MYSQL_USER: prepflow + MYSQL_PASSWORD: prepflow + MYSQL_ROOT_PASSWORD: prepflow_root + ports: + - "3307:3306" + volumes: + - prepflow_mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uprepflow -pprepflow --silent"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 30s + + mailpit: + image: axllent/mailpit:v1.20 + ports: + - "1025:1025" + - "8025:8025" + + api: + build: + context: . + dockerfile: api/Dockerfile.dev + depends_on: + mysql: + condition: service_healthy + environment: + APP_NAME: PrepFlow + APP_ENV: local + APP_DEBUG: "true" + APP_URL: http://localhost:8000 + FRONTEND_URL: http://localhost:5173 + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: "3306" + DB_DATABASE: prepflow + DB_USERNAME: prepflow + DB_PASSWORD: prepflow + SESSION_DRIVER: database + SESSION_DOMAIN: "null" + SESSION_SAME_SITE: lax + SESSION_SECURE_COOKIE: "false" + CACHE_STORE: database + QUEUE_CONNECTION: database + MAIL_MAILER: smtp + MAIL_HOST: mailpit + MAIL_PORT: "1025" + MAIL_USERNAME: "null" + MAIL_PASSWORD: "null" + MAIL_FROM_ADDRESS: hello@prepflow.local + MAIL_FROM_NAME: PrepFlow + CORS_ALLOWED_ORIGINS: http://localhost,http://localhost:5173,http://127.0.0.1:5173,http://10.0.2.2,https://localhost + CLOUDINARY_URL: ${CLOUDINARY_URL:-} + OCR_TECHNIQUE: tesseract + TESSERACT_BIN: /usr/bin/tesseract + RUN_MIGRATIONS: "true" + RUN_SEEDERS: "true" + ports: + - "8000:80" + volumes: + - ./api:/var/www/html + - prepflow_api_storage:/var/www/html/storage + - prepflow_api_cache:/var/www/html/bootstrap/cache + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost/api/menu >/dev/null || exit 1"] + interval: 10s + timeout: 10s + retries: 30 + start_period: 240s + + web: + image: oven/bun:1.3 + depends_on: + api: + condition: service_healthy + working_dir: /workspace + environment: + VITE_API_URL: /api + VITE_DEV_API_PROXY: http://api + command: sh -lc "bun install --frozen-lockfile && bun run --cwd apps/web dev --host 0.0.0.0" + ports: + - "5173:5173" + volumes: + - .:/workspace + - prepflow_node_modules:/workspace/node_modules + - prepflow_bun_cache:/root/.bun/install/cache + + adminer: + image: adminer:4 + profiles: + - tools + depends_on: + mysql: + condition: service_healthy + environment: + ADMINER_DEFAULT_SERVER: mysql + ports: + - "8081:8080" + +volumes: + prepflow_mysql_data: + prepflow_api_storage: + prepflow_api_cache: + prepflow_node_modules: + prepflow_bun_cache: diff --git a/docs/development/docker-local.md b/docs/development/docker-local.md new file mode 100644 index 0000000..de45934 --- /dev/null +++ b/docs/development/docker-local.md @@ -0,0 +1,146 @@ +# Docker Local Development + +Docker is an optional local development path for PrepFlow. It does not replace +the working production deployment: + +- Vercel still serves the public web app. +- Render still runs the production Laravel Docker service. +- TiDB Cloud remains the production database. +- Android Studio is still required for APK builds and device testing. + +The Docker stack is for repeatable local backend/web/OCR/email testing without +depending on XAMPP state. + +## Services + +| Service | URL / port | Purpose | +|---|---|---| +| `web` | http://localhost:5173 | Vite + React via Bun | +| `api` | http://localhost:8000 | Laravel API, Apache, PDO MySQL, Tesseract | +| `mysql` | `127.0.0.1:3307` | Local MySQL 8.4 database, separate from XAMPP | +| `mailpit` | http://localhost:8025 | Local email inbox; SMTP listens on `1025` | +| `adminer` | http://localhost:8081 | Optional DB browser; run with the `tools` profile | + +Local database credentials are intentionally low-value dev credentials: + +```text +host: 127.0.0.1 +port: 3307 +database: prepflow +username: prepflow +password: prepflow +``` + +Do not use these in production. + +## Start + +From the repository root: + +```powershell +docker compose up --build +``` + +The first start builds the dev API image, installs Composer dependencies into +the gitignored `api/vendor` directory, starts MySQL, runs migrations, seeds +demo data, starts Mailpit, and starts Vite. + +Open: + +- Web: http://localhost:5173 +- API health: http://localhost:8000/api/menu +- Mailpit: http://localhost:8025 + +Seeded demo accounts: + +| Role | Email | Password | +|---|---|---| +| Admin | `admin@thaijoint.ph` | `prepflow123` | +| Customer | `maria@example.com` | `prepflow123` | + +## Optional DB Browser + +```powershell +docker compose --profile tools up -d adminer +``` + +Open http://localhost:8081 and use: + +```text +System: MySQL +Server: mysql +Username: prepflow +Password: prepflow +Database: prepflow +``` + +## Common Commands + +```powershell +# See service state +docker compose ps + +# Laravel tests inside the dev API container +docker compose exec api prepflow-test + +# Laravel Pint inside the dev API container +docker compose exec api vendor/bin/pint --test + +# Fresh local DB reset with seeded demo data +docker compose exec api php artisan migrate:fresh --seed + +# Run the OCR diagnostic with a file already mounted under api/ +docker compose exec api php artisan ocr:receipt-check --image-file=storage/app/sample-receipt.jpg + +# Stop containers, keep DB/dependency volumes +docker compose down + +# Full reset: remove DB and dependency volumes +docker compose down -v +``` + +`api/vendor` is intentionally a normal gitignored directory instead of a Docker +volume. Composer may need to delete and recreate it after a failed dist +download, and bind-mounting that exact path as a volume breaks that recovery +path on Docker Desktop for Windows. + +## Cloudinary Upload Testing + +The compose stack does not commit or bake Cloudinary secrets. If you need menu +image, QR image, or receipt upload tests against Cloudinary, set +`CLOUDINARY_URL` in your shell before starting Docker, or put it in an +untracked root `.env` file that Docker Compose reads automatically. + +PowerShell shell variable: + +```powershell +$env:CLOUDINARY_URL='cloudinary://:@' +docker compose up --build +``` + +Never commit `.env` or real provider credentials. + +## Windows Docker CLI Note + +If Docker Desktop is installed but `docker` is not recognized in a new +PowerShell/Codex shell, restart the shell first. If it is still missing, add +Docker Desktop's CLI directory to `PATH`: + +```text +C:\Program Files\Docker\Docker\resources\bin +``` + +You can verify the CLI directly with: + +```powershell +& 'C:\Program Files\Docker\Docker\resources\bin\docker.exe' --version +& 'C:\Program Files\Docker\Docker\resources\bin\docker.exe' compose version +``` + +## Production Guardrails + +- Do not deploy `compose.yaml` to production; it is local-dev tooling. +- Do not replace Render's `api/Dockerfile` with `api/Dockerfile.dev`. +- The browser web app uses `/api` through Vite/Vercel proxies. +- Production APK builds still use the direct Render API. +- Keep secrets out of docs, Git, Docker images, and committed env files.