diff --git a/.gitea/workflows/build-deploy.yml b/.gitea/workflows/build-deploy.yml new file mode 100644 index 0000000..bbf283f --- /dev/null +++ b/.gitea/workflows/build-deploy.yml @@ -0,0 +1,47 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: code.puffinoffset.com + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: code.puffinoffset.com/matt/puffin-app + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=code.puffinoffset.com/matt/puffin-app:buildcache + cache-to: type=registry,ref=code.puffinoffset.com/matt/puffin-app:buildcache,mode=max + + - name: Image digest + run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}" diff --git a/CLAUDE.md b/CLAUDE.md index 07a692e..ac9dc07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,11 +92,58 @@ VITE_FORMSPREE_OFFSET_ID= # Offset order form endpoint ### Testing Strategy - Unit tests using Vitest and React Testing Library -- Test files in `__tests__` directories -- Run with `npm test` +- Test configuration in `vitest.config.ts` with jsdom environment +- Setup file at `src/test/setup.ts` +- Test files use pattern `*.test.ts` or `*.test.tsx` +- Run all tests: `npm test` +- Tests run with global test APIs enabled ### Build & Deployment -- Production builds output to `dist/` +- Production builds output to `dist/` directory +- Vite build configuration includes: + - Source maps enabled for debugging + - Code splitting with vendor chunk (React/React-DOM) + - Lucide-react excluded from optimizeDeps for compatibility - Docker deployment uses Nginx to serve static files on port 3800 - Host Nginx reverse proxy configuration available in `nginx-host.conf` -- PWA manifest and service worker for mobile app installation \ No newline at end of file +- PWA manifest (`public/manifest.json`) and service worker (`public/sw.js`) for mobile app installation + +### CI/CD Pipeline + +**Automated Builds**: +- Gitea Actions workflow at `.gitea/workflows/build-deploy.yml` +- Triggers on push to `main` branch +- Builds multi-stage Docker image (Node build → Nginx serve) +- Pushes to Gitea container registry at `code.puffinoffset.com/matt/puffin-app` + +**Image Tagging Strategy**: +- `latest`: Always points to most recent main branch build +- `main-`: Specific commit version for rollbacks + +**Container Registry**: +- Gitea's built-in Docker registry +- Authentication via Gitea credentials +- Registry URL: `code.puffinoffset.com` + +**Deployment**: +- Manual deployment via Portainer +- Use `docker-compose.portainer.yml` for production stack configuration +- Environment variables mounted via `.env` file volume +- Detailed deployment instructions in `DEPLOYMENT.md` + +**Workflow Optimization**: +- Docker buildx for multi-platform support +- Layer caching to speed up builds +- Separate build cache stored in registry + +### Key Implementation Details + +**Routing without React Router**: App uses state-based routing via `currentPage` state and `window.history.pushState()`. The `/mobile-app` route is detected by checking `window.location.pathname` and renders only `MobileCalculator` without the standard layout. + +**Sample Vessel Data**: A hardcoded `sampleVessel` object is used as default vessel data in `App.tsx` (lines 17-24) since AIS client currently returns mock data. + +**Analytics Integration**: `utils/analytics.ts` tracks page views via Google Analytics. Called in `App.tsx` useEffect when route changes. + +**Currency Support**: `utils/currencies.ts` provides multi-currency conversion. Calculator components support USD, EUR, GBP selection. + +**Error Handling**: `ErrorBoundary.tsx` component wraps the app to catch React rendering errors gracefully. \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..8e23abf --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,243 @@ +# Puffin Offset - Deployment Guide + +This guide covers deploying the Puffin Offset application using Gitea Actions for automated builds and Portainer for container orchestration. + +## Architecture Overview + +``` +Push to main → Gitea Actions → Build Docker Image → Push to Registry → Manual Deploy via Portainer +``` + +## CI/CD Pipeline + +### Automated Build Process + +When code is pushed to the `main` branch: + +1. **Gitea Actions triggers** automatically +2. **Docker image is built** using the multi-stage Dockerfile +3. **Image is pushed** to Gitea's container registry with two tags: + - `latest` - Always points to the most recent build + - `main-` - Specific commit for rollback capability + +### Registry Location + +Images are stored at: +``` +code.puffinoffset.com/matt/puffin-app:latest +code.puffinoffset.com/matt/puffin-app:main- +``` + +## Portainer Deployment + +### Prerequisites + +1. **Portainer installed and accessible** +2. **Gitea registry credentials configured in Portainer** +3. **Environment variables prepared** (see below) + +### Step 1: Configure Gitea Registry in Portainer + +1. Navigate to **Portainer → Registries** +2. Click **"Add registry"** +3. Configure: + - **Name**: `Gitea - code.puffinoffset.com` + - **Registry URL**: `code.puffinoffset.com` + - **Authentication**: Enabled + - **Username**: Your Gitea username (e.g., `matt`) + - **Password**: Gitea access token or password +4. Click **"Add registry"** + +### Step 2: Prepare Environment Variables + +Create a `.env` file on your server with the required variables: + +```bash +# Example: /path/to/puffin-app/.env +VITE_WREN_API_TOKEN=your-wren-api-token +VITE_FORMSPREE_CONTACT_ID=your-formspree-contact-id +VITE_FORMSPREE_OFFSET_ID=your-formspree-offset-id +``` + +**Note**: The env.sh script in the Docker image will inject these at runtime. + +### Step 3: Create Portainer Stack + +#### Option A: Using Portainer UI + +1. Navigate to **Portainer → Stacks** +2. Click **"Add stack"** +3. Configure: + - **Name**: `puffin-app` + - **Build method**: "Web editor" +4. Paste the contents of `docker-compose.portainer.yml` +5. In **Environment variables** section, add: + - Name: `ENV_FILE_PATH` + - Value: `/path/to/your/.env` +6. Click **"Deploy the stack"** + +#### Option B: Using Git Repository + +1. Navigate to **Portainer → Stacks** +2. Click **"Add stack"** +3. Configure: + - **Name**: `puffin-app` + - **Build method**: "Repository" + - **Repository URL**: `https://code.puffinoffset.com/matt/puffin-app.git` + - **Repository reference**: `refs/heads/main` + - **Compose path**: `docker-compose.portainer.yml` + - **Authentication**: Configure with your Gitea credentials +4. Click **"Deploy the stack"** + +### Step 4: Verify Deployment + +1. Check that the container is running: + - Navigate to **Portainer → Containers** + - Look for `puffin-app-web-1` or similar + - Status should be "running" + +2. Test the application: + - Access: `http://your-server:3800` + - Or via your Nginx reverse proxy configuration + +3. Check logs if needed: + - Click on the container + - Select **"Logs"** tab + +## Updating to New Versions + +### Method 1: Using Portainer UI (Recommended) + +1. Navigate to **Portainer → Stacks** +2. Click on **puffin-app** stack +3. Click **"Update the stack"** +4. Enable **"Pull latest image version"** +5. Enable **"Re-pull image and redeploy"** +6. Click **"Update"** + +Portainer will: +- Pull the latest image from Gitea registry +- Recreate the container with the new image +- Maintain your environment variables and configuration + +### Method 2: Pull Specific Version + +To rollback or deploy a specific commit: + +1. Edit the stack in Portainer +2. Change the image tag from `latest` to `main-` + ```yaml + image: code.puffinoffset.com/matt/puffin-app:main-abc1234 + ``` +3. Update the stack + +## Monitoring Build Status + +### Check Gitea Actions + +1. Go to your Gitea repository: `https://code.puffinoffset.com/matt/puffin-app` +2. Navigate to **"Actions"** tab +3. View recent workflow runs +4. Click on a run to see detailed logs + +### Verify Image in Registry + +1. In Gitea, navigate to **"Packages"** or **"Registry"** +2. Look for `puffin-app` images +3. Verify tags: `latest` and `main-` tags should be present + +## Troubleshooting + +### Build Failed in Gitea Actions + +**Check the workflow logs:** +1. Go to Gitea → puffin-app → Actions +2. Click on the failed run +3. Review error messages + +**Common issues:** +- Registry authentication failed: Check secrets.GITHUB_TOKEN is available +- Build errors: Review Dockerfile and build logs +- Network issues: Check runner connectivity + +### Image Pull Failed in Portainer + +**Error:** `unauthorized: authentication required` + +**Solution:** +1. Verify registry is configured in Portainer (Step 1) +2. Check credentials are correct +3. Ensure registry URL is `code.puffinoffset.com` (no `https://`) + +### Container Won't Start + +**Check environment variables:** +1. Verify `.env` file exists on the host +2. Ensure volume mount path is correct in docker-compose.portainer.yml +3. Check container logs for missing env var errors + +**Check port conflicts:** +1. Ensure port 3800 is not already in use +2. Run `netstat -tuln | grep 3800` on the host + +### Application Not Accessible + +**Verify container is running:** +```bash +docker ps | grep puffin-app +``` + +**Check nginx reverse proxy:** +- Review host Nginx configuration (`nginx-host.conf`) +- Ensure proxy_pass points to correct container port +- Test direct access: `http://server-ip:3800` + +**Check firewall:** +```bash +# Example for ufw +sudo ufw status +sudo ufw allow 3800/tcp +``` + +## Environment Variables Reference + +| Variable | Description | Required | +|----------|-------------|----------| +| `VITE_WREN_API_TOKEN` | Wren Climate API authentication token | Yes | +| `VITE_FORMSPREE_CONTACT_ID` | Formspree contact form endpoint ID | Yes | +| `VITE_FORMSPREE_OFFSET_ID` | Formspree offset order form endpoint ID | Yes | +| `NODE_ENV` | Node environment (set to `production`) | Auto-set | + +## Rollback Procedure + +To rollback to a previous version: + +1. Find the commit SHA of the working version +2. In Portainer, edit the stack +3. Change image tag to `main-` +4. Update the stack + +Example: +```yaml +# Before (current broken version) +image: code.puffinoffset.com/matt/puffin-app:latest + +# After (rollback to specific commit) +image: code.puffinoffset.com/matt/puffin-app:main-ab0dbbd +``` + +## Security Best Practices + +1. **Never commit `.env` file to git** (already in .gitignore) +2. **Use Gitea access tokens** instead of passwords for registry auth +3. **Restrict registry access** to necessary users +4. **Review Gitea Actions logs** for sensitive data exposure +5. **Keep base images updated** (node:20-alpine, nginx:alpine) + +## Next Steps + +- Set up monitoring/alerting for failed builds +- Configure automatic backups of `.env` file +- Implement health checks in docker-compose +- Set up SSL certificates via Let's Encrypt +- Configure log aggregation for production debugging diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml new file mode 100644 index 0000000..0100414 --- /dev/null +++ b/docker-compose.portainer.yml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + web: + image: code.puffinoffset.com/matt/puffin-app:latest + ports: + - "3800:3800" + environment: + - NODE_ENV=production + restart: unless-stopped + volumes: + # Mount .env file for runtime environment variable injection + - ./.env:/usr/share/nginx/html/.env:ro + # Optional: override nginx config if needed + # - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + +# Production deployment notes: +# 1. Ensure .env file exists on the host with required variables: +# - VITE_WREN_API_TOKEN +# - VITE_FORMSPREE_CONTACT_ID +# - VITE_FORMSPREE_OFFSET_ID +# +# 2. Configure Gitea registry authentication in Portainer before deploying +# +# 3. To update to new image: +# - Navigate to stack in Portainer +# - Click "Update the stack" +# - Enable "Pull latest image version" +# - Click "Update"