From c9cd00a0558cdb2d432da5b1c06e5aad823717f1 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 19:58:43 -0700 Subject: [PATCH 01/17] fix: handle existing git tags and use DOCKER_LOGIN/DOCKER_PASSWORD secrets --- .gitea/workflows/release.yml | 32 ++++++++++++++++++++++++-------- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- scripts/bump-version.js | 1 + 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 70d567f..cd48527 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -62,19 +62,35 @@ jobs: - name: Create git tag for release run: | - git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" - git push origin "v${{ steps.version.outputs.version }}" + TAG_NAME="v${{ steps.version.outputs.version }}" + # Check if tag already exists + if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then + echo "Tag $TAG_NAME already exists, skipping creation" + else + echo "Creating tag $TAG_NAME" + git tag -a "$TAG_NAME" -m "Release $TAG_NAME" + fi + # Always try to push (in case tag exists locally but not remotely) + git push origin "$TAG_NAME" || echo "Tag push failed (may already exist remotely)" - name: Push Docker images run: | echo "Pushing to ${{ env.REGISTRY }}..." - # Check if we can authenticate to the registry - if docker login ${{ env.REGISTRY }} -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 2>/dev/null; then - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - echo "Successfully pushed images to ${{ env.REGISTRY }}" + # Check if we can authenticate to the registry using DOCKER_LOGIN and DOCKER_PASSWORD secrets + if [ -n "${{ secrets.DOCKER_LOGIN }}" ] && [ -n "${{ secrets.DOCKER_PASSWORD }}" ]; then + if docker login ${{ env.REGISTRY }} -u ${{ secrets.DOCKER_LOGIN }} -p ${{ secrets.DOCKER_PASSWORD }} 2>/dev/null; then + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + echo "Successfully pushed images to ${{ env.REGISTRY }}" + else + echo "Warning: Docker login failed with provided credentials" + echo "Images built locally but not pushed to registry" + echo "Manual push required:" + echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" + echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" + fi else - echo "Warning: Could not authenticate to ${{ env.REGISTRY }}" + echo "Warning: DOCKER_LOGIN or DOCKER_PASSWORD secrets not configured" echo "Images built locally but not pushed to registry" echo "Manual push required:" echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d6c33..8b25b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## [0.1.2] - 2026-04-01 + +### Patch Changes + +- fix: handle Docker registry authentication gracefully in release workflow +- fix: run unit tests on branch commits, skip main branch +- feat: add test workflow for every commit +- trigger: release with test-capable image +- feat: build test-capable image, run tests, then build production image +- trigger: release workflow +- fix: release workflow should not commit, only tag +- trigger: manual workflow trigger for docker.notsosm.art +- fix: update Docker build script to use docker.notsosm.art registry +- fix: update workflow to use docker.notsosm.art registry +- trigger: manual workflow trigger +- fix: update workflow to use correct Docker registries +- feat: add Gitea Actions release workflow +- feat: add view match link to admin matches page + ## [0.1.1] - 2026-04-01 ### Patch Changes diff --git a/package.json b/package.json index cdf1f63..c66dbc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "euchre_camp", - "version": "0.1.1", + "version": "0.1.2", "private": true, "scripts": { "dev": "NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse --short HEAD) next dev", diff --git a/scripts/bump-version.js b/scripts/bump-version.js index 8012174..989a4b7 100755 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -17,6 +17,7 @@ const path = require('path'); // Parse command line arguments const args = process.argv.slice(2); const forcedVersion = args[0]; // e.g., "0.2.0" or "patch/minor/major" +const skipConfirm = args.includes('--yes') || args.includes('-y'); // Skip confirmation prompt // Paths const packageJsonPath = path.join(__dirname, '..', 'package.json'); -- 2.52.0 From 73ba929e3674e8300f90c04ef91dcff07f9ce7ff Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 20:01:24 -0700 Subject: [PATCH 02/17] fix: update release workflow to bump version on PR merge --- .gitea/workflows/release.yml | 106 +++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index cd48527..b64aaa4 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -10,8 +10,9 @@ env: IMAGE_NAME: euchre-camp jobs: - build-and-test: + release: runs-on: ubuntu-latest + # Skip if this is an auto-generated version bump commit if: "!contains(github.event.head_commit.message, 'chore: bump version')" steps: @@ -19,14 +20,75 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.GITEA_TOKEN || github.token }} - - name: Get current version + - name: Configure Git + run: | + git config user.name "Gitea Actions" + git config user.email "actions@gitea.com" + + - name: Determine version bump type + id: bump_type + run: | + # Get the merge commit message or use commits since last tag + MERGE_MSG="${{ github.event.head_commit.message }}" + echo "Commit message: $MERGE_MSG" + + # Determine bump type from commit message or commits + if echo "$MERGE_MSG" | grep -qE "(BREAKING CHANGE|!:)"; then + echo "bump=major" >> $GITHUB_OUTPUT + echo "Bump type: major (breaking change detected)" + elif echo "$MERGE_MSG" | grep -qE "^feat"; then + echo "bump=minor" >> $GITHUB_OUTPUT + echo "Bump type: minor (feature detected)" + else + # Check actual commits in the PR + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$LAST_TAG" ]; then + COMMITS=$(git log --oneline ${LAST_TAG}..HEAD 2>/dev/null | head -5) + else + COMMITS=$(git log --oneline | head -5) + fi + echo "Recent commits: $COMMITS" + + if echo "$COMMITS" | grep -qE "BREAKING\|!:"; then + echo "bump=major" >> $GITHUB_OUTPUT + echo "Bump type: major (breaking change in commits)" + elif echo "$COMMITS" | grep -qE "^feat"; then + echo "bump=minor" >> $GITHUB_OUTPUT + echo "Bump type: minor (feature in commits)" + else + echo "bump=patch" >> $GITHUB_OUTPUT + echo "Bump type: patch (default)" + fi + fi + + - name: Bump version id: version run: | - # Read version from package.json - CURRENT_VERSION=$(node -p "require('./package.json').version") - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - echo "Current version: $CURRENT_VERSION" + BUMP="${{ steps.bump_type.outputs.bump }}" + echo "Bumping version: $BUMP" + + # Run the bump script + node scripts/bump-version.js "$BUMP" --yes + + # Get new version + NEW_VERSION=$(node -p "require('./package.json').version") + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "New version: $NEW_VERSION" + + - name: Commit version bump + run: | + git add package.json CHANGELOG.md + git commit -m "chore: bump version to v${{ steps.version.outputs.new_version }}" + git push origin main + + - name: Create git tag for release + run: | + TAG_NAME="v${{ steps.version.outputs.new_version }}" + echo "Creating tag $TAG_NAME" + git tag -a "$TAG_NAME" -m "Release $TAG_NAME" + git push origin "$TAG_NAME" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -36,14 +98,14 @@ jobs: docker build \ --target test-runner \ --build-arg GIT_COMMIT=${{ github.sha }} \ - -t ${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.version }} \ + -t ${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \ . - name: Run tests inside test-capable container run: | docker run --rm \ -e DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" \ - ${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.version }} \ + ${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \ npm run test:run - name: Build production image @@ -51,53 +113,35 @@ jobs: docker build \ --target runner \ --build-arg GIT_COMMIT=${{ github.sha }} \ - -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }} \ -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ . - - name: Configure Git - run: | - git config user.name "Gitea Actions" - git config user.email "actions@gitea.com" - - - name: Create git tag for release - run: | - TAG_NAME="v${{ steps.version.outputs.version }}" - # Check if tag already exists - if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then - echo "Tag $TAG_NAME already exists, skipping creation" - else - echo "Creating tag $TAG_NAME" - git tag -a "$TAG_NAME" -m "Release $TAG_NAME" - fi - # Always try to push (in case tag exists locally but not remotely) - git push origin "$TAG_NAME" || echo "Tag push failed (may already exist remotely)" - - name: Push Docker images run: | echo "Pushing to ${{ env.REGISTRY }}..." # Check if we can authenticate to the registry using DOCKER_LOGIN and DOCKER_PASSWORD secrets if [ -n "${{ secrets.DOCKER_LOGIN }}" ] && [ -n "${{ secrets.DOCKER_PASSWORD }}" ]; then if docker login ${{ env.REGISTRY }} -u ${{ secrets.DOCKER_LOGIN }} -p ${{ secrets.DOCKER_PASSWORD }} 2>/dev/null; then - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }} docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest echo "Successfully pushed images to ${{ env.REGISTRY }}" else echo "Warning: Docker login failed with provided credentials" echo "Images built locally but not pushed to registry" echo "Manual push required:" - echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" + echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }}" echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" fi else echo "Warning: DOCKER_LOGIN or DOCKER_PASSWORD secrets not configured" echo "Images built locally but not pushed to registry" echo "Manual push required:" - echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" + echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }}" echo " docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" fi - name: Deploy to dev (placeholder) run: | - echo "Deploying version ${{ steps.version.outputs.version }} to dev environment..." + echo "Deploying version ${{ steps.version.outputs.new_version }} to dev environment..." # TODO: Add actual deployment steps -- 2.52.0 From 5c9b98a926b0840bf1551d50a9a7943547f8dabf Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 20:01:41 -0700 Subject: [PATCH 03/17] feat: add PR workflow with bump type analysis --- .gitea/workflows/pr.yml | 91 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .gitea/workflows/pr.yml diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml new file mode 100644 index 0000000..ba1d989 --- /dev/null +++ b/.gitea/workflows/pr.yml @@ -0,0 +1,91 @@ +name: Pull Request + +on: + pull_request: + branches: + - main + +jobs: + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm run test:run + + analyze-bump-type: + runs-on: ubuntu-latest + needs: unit-tests + if: success() + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Analyze commits for bump type + id: bump_type + run: | + # Get the merge base and PR commits + git fetch origin main:main 2>/dev/null || true + MERGE_BASE=$(git merge-base HEAD main 2>/dev/null || echo "HEAD~1") + + # Analyze commits in this PR + COMMITS=$(git log --oneline ${MERGE_BASE}..HEAD 2>/dev/null || git log --oneline -10) + + echo "Commits in this PR:" + echo "$COMMITS" + echo "" + + # Determine bump type + if echo "$COMMITS" | grep -qE "(BREAKING CHANGE|!:)"; then + BUMP="major" + REASON="Breaking change detected in commit messages" + elif echo "$COMMITS" | grep -qE "^feat"; then + BUMP="minor" + REASON="Feature commits detected" + else + BUMP="patch" + REASON="Defaulting to patch (fixes or other changes)" + fi + + echo "Suggested bump type: $BUMP" + echo "Reason: $REASON" + echo "bump=$BUMP" >> $GITHUB_OUTPUT + echo "reason=$REASON" >> $GITHUB_OUTPUT + + - name: Comment bump type on PR + uses: actions/github-script@v7 + with: + script: | + const bump = '${{ steps.bump_type.outputs.bump }}'; + const reason = '${{ steps.bump_type.outputs.reason }}'; + + const comment = ` + ## 🏷️ Version Bump Analysis + + **Suggested bump type:** \`${bump.toUpperCase()}\` + **Reason:** ${reason} + + This PR will bump the version to the next ${bump} version when merged. + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); -- 2.52.0 From 9b69cb84e36a50b22616f5f325411a37464a646e Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 20:02:34 -0700 Subject: [PATCH 04/17] docs: add workflow architecture documentation --- .gitea/WORKFLOW_ARCHITECTURE.md | 124 ++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .gitea/WORKFLOW_ARCHITECTURE.md diff --git a/.gitea/WORKFLOW_ARCHITECTURE.md b/.gitea/WORKFLOW_ARCHITECTURE.md new file mode 100644 index 0000000..7490a1c --- /dev/null +++ b/.gitea/WORKFLOW_ARCHITECTURE.md @@ -0,0 +1,124 @@ +# Gitea Actions Workflow Architecture + +This document describes the workflow architecture for version bumping and releases. + +## Overview + +The workflow architecture uses a two-step process: +1. **PR Workflow**: Analyzes commits and suggests bump type +2. **Release Workflow**: Bumps version, creates tags, and builds Docker images on merge + +## Workflow Files + +### 1. `.gitea/workflows/pr.yml` (Pull Request Workflow) + +**Trigger**: Pull requests to `main` branch + +**Purpose**: +- Run unit tests on every PR +- Analyze commits to determine bump type (major/minor/patch) +- Comment the suggested bump type on the PR + +**Bump Type Detection**: +- **Major**: Breaking changes detected (`BREAKING CHANGE` or `!:` in commit messages) +- **Minor**: Feature commits detected (`feat:` prefix) +- **Patch**: Default for fixes and other changes + +### 2. `.gitea/workflows/release.yml` (Release Workflow) + +**Trigger**: Pushes to `main` branch (after PR merge) + +**Purpose**: +- Determine bump type from merge commit or PR commits +- Bump version in `package.json` and `CHANGELOG.md` +- Commit the version bump +- Create git tag for the release +- Run tests inside Docker container +- Build production Docker image +- Push images to registry +- Deploy to dev environment + +**Key Features**: +- Skips commits that are auto-generated version bumps +- Uses `DOCKER_LOGIN` and `DOCKER_PASSWORD` secrets for registry auth +- Handles existing git tags gracefully + +## Version Bump Logic + +### Step 1: PR Analysis (pr.yml) +When a PR is opened or updated: +1. Fetch the merge base with `main` +2. Analyze all commits in the PR +3. Determine bump type based on commit messages: + - Breaking changes → major + - Features → minor + - Fixes → patch +4. Comment the suggested bump type on the PR + +### Step 2: Release (release.yml) +When a PR is merged to `main`: +1. Check if the commit is an auto-bump (skip if so) +2. Analyze the merge commit message or PR commits +3. Run `node scripts/bump-version.js --yes` +4. Commit the version bump changes +5. Create and push git tag +6. Build and push Docker images + +## Environment Variables & Secrets + +### Required Secrets +- `DOCKER_LOGIN`: Username for Docker registry authentication +- `DOCKER_PASSWORD`: Password for Docker registry authentication +- `GITEA_TOKEN` (optional): For pushing back to repo (if needed) + +### Environment Variables +- `REGISTRY`: Docker registry URL (default: `docker.notsosm.art`) +- `IMAGE_NAME`: Docker image name (default: `euchre-camp`) + +## Example Workflow + +### Scenario: Feature PR +1. Developer opens PR with commits: + - "feat: add new feature" + - "fix: resolve edge case" +2. PR workflow runs: + - Unit tests pass + - Bump type analysis suggests "minor" + - Comment posted on PR: "Suggested bump: MINOR" +3. Developer merges PR +4. Release workflow runs: + - Detects minor bump from commits + - Bumps version from 0.1.1 → 0.2.0 + - Commits version bump + - Creates tag v0.2.0 + - Builds and pushes Docker image + - Deploys to dev + +### Scenario: Breaking Change PR +1. Developer opens PR with commit: + - "feat!: breaking API change" +2. PR workflow runs: + - Detects breaking change marker + - Suggests "major" bump + - Comments on PR +3. Developer merges PR +4. Release workflow runs: + - Detects major bump + - Bumps version from 0.1.1 → 1.0.0 + - Creates tag v1.0.0 + - Proceeds with build and deploy + +## Benefits + +1. **No CI Loops**: Version bump commits are detected and skipped +2. **Clear Communication**: PR comments inform developers of impact +3. **Semantic Versioning**: Automated adherence to semver rules +4. **Traceability**: Git tags and changelog reflect all changes +5. **Safe Releases**: Tests run before version bump and deployment + +## Future Enhancements + +- Add GitHub/Gitea Release creation +- Slack/Discord notifications on release +- Automatic rollback on test failure +- Multi-environment deployment (dev/staging/prod) -- 2.52.0 From ff7461973a97c4bbe82a673b1472b71296c1913e Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 20:05:48 -0700 Subject: [PATCH 05/17] fix: optimize workflows to avoid redundant test runs --- .gitea/workflows/pr.yml | 21 --------------------- .gitea/workflows/test.yml | 28 ---------------------------- 2 files changed, 49 deletions(-) diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml index ba1d989..9689188 100644 --- a/.gitea/workflows/pr.yml +++ b/.gitea/workflows/pr.yml @@ -6,29 +6,8 @@ on: - main jobs: - unit-tests: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run unit tests - run: npm run test:run - analyze-bump-type: runs-on: ubuntu-latest - needs: unit-tests - if: success() steps: - name: Checkout code diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 7ba1182..1455602 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -5,9 +5,6 @@ on: branches: - '**' - '!main' # Skip main branch (release workflow handles that) - pull_request: - branches: - - main jobs: unit-tests: @@ -28,28 +25,3 @@ jobs: - name: Run unit tests run: npm run test:run - - # Acceptance tests that don't require DB read/write - # Only run on pull requests to main - safe-acceptance-tests: - runs-on: ubuntu-latest - needs: unit-tests - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run safe acceptance tests - # These tests should not read/write to the database - # They might test UI components, routing, or static content - run: npm run test:acceptance -- --grep "safe|static|ui|component" -- 2.52.0 From 6b70c3f38812abc1dff1afc4cb5298eba718bb62 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:48:39 -0700 Subject: [PATCH 06/17] ci: add unit and acceptance tests to PR workflow with SQLite --- .gitea/WORKFLOW_ARCHITECTURE.md | 57 +++++++++++++++++++++++++++++---- .gitea/workflows/pr.yml | 57 +++++++++++++++++++++++++++++++++ .gitea/workflows/test.yml | 3 +- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/.gitea/WORKFLOW_ARCHITECTURE.md b/.gitea/WORKFLOW_ARCHITECTURE.md index 7490a1c..3cdaff6 100644 --- a/.gitea/WORKFLOW_ARCHITECTURE.md +++ b/.gitea/WORKFLOW_ARCHITECTURE.md @@ -1,12 +1,13 @@ # Gitea Actions Workflow Architecture -This document describes the workflow architecture for version bumping and releases. +This document describes the workflow architecture for version bumping, testing, and releases. ## Overview -The workflow architecture uses a two-step process: -1. **PR Workflow**: Analyzes commits and suggests bump type -2. **Release Workflow**: Bumps version, creates tags, and builds Docker images on merge +The workflow architecture uses a three-step process: +1. **PR Workflow**: Runs tests on PRs and analyzes commits for bump type +2. **Test Workflow**: Runs unit tests on all branch pushes (except version bumps) +3. **Release Workflow**: Bumps version, creates tags, builds Docker images, and deploys on main branch pushes ## Workflow Files @@ -15,16 +16,36 @@ The workflow architecture uses a two-step process: **Trigger**: Pull requests to `main` branch **Purpose**: -- Run unit tests on every PR +- Run unit tests on every PR (fast feedback) +- Run acceptance tests with SQLite database - Analyze commits to determine bump type (major/minor/patch) - Comment the suggested bump type on the PR +**Test Execution**: +- **Unit Tests**: Run first, fast execution +- **Acceptance Tests**: Run after unit tests pass, uses SQLite database +- **Database**: SQLite with `DATABASE_URL=file:./prisma/ci.db` +- **Secrets**: Uses `BETTER_AUTH_SECRET` for authentication + **Bump Type Detection**: - **Major**: Breaking changes detected (`BREAKING CHANGE` or `!:` in commit messages) - **Minor**: Feature commits detected (`feat:` prefix) - **Patch**: Default for fixes and other changes -### 2. `.gitea/workflows/release.yml` (Release Workflow) +### 2. `.gitea/workflows/test.yml` (Test Workflow) + +**Trigger**: Pushes to any branch (including main) + +**Purpose**: +- Run unit tests on all branch pushes +- Skip auto-generated version bump commits (handled by release workflow) + +**Key Features**: +- Runs on all branches including main +- Skips commits with "chore: bump version" message +- Fast execution for quick feedback + +### 3. `.gitea/workflows/release.yml` (Release Workflow) **Trigger**: Pushes to `main` branch (after PR merge) @@ -33,7 +54,7 @@ The workflow architecture uses a two-step process: - Bump version in `package.json` and `CHANGELOG.md` - Commit the version bump - Create git tag for the release -- Run tests inside Docker container +- Run tests inside Docker container (with PostgreSQL) - Build production Docker image - Push images to registry - Deploy to dev environment @@ -42,6 +63,7 @@ The workflow architecture uses a two-step process: - Skips commits that are auto-generated version bumps - Uses `DOCKER_LOGIN` and `DOCKER_PASSWORD` secrets for registry auth - Handles existing git tags gracefully +- Runs comprehensive tests in production-like environment ## Version Bump Logic @@ -108,6 +130,25 @@ When a PR is merged to `main`: - Creates tag v1.0.0 - Proceeds with build and deploy +## Database Configuration for CI + +### SQLite for CI Acceptance Tests +- **Why SQLite**: No database server required, perfect for CI environments +- **Usage**: PR workflow runs acceptance tests with SQLite database +- **Configuration**: `DATABASE_PROVIDER=sqlite`, `DATABASE_URL=file:./prisma/ci.db` +- **Benefits**: Fast, isolated, no external dependencies + +### PostgreSQL for Production +- **Usage**: Release workflow runs tests in Docker with PostgreSQL +- **Configuration**: Uses dummy PostgreSQL URL for Docker builds +- **Benefits**: Production-like environment, catches PostgreSQL-specific issues + +### Database Provider Detection +The application automatically detects the database provider: +- `DATABASE_PROVIDER` environment variable (defaults to `sqlite`) +- `prisma.ts` conditionally uses PrismaPg adapter for PostgreSQL +- Better Auth configured with appropriate provider + ## Benefits 1. **No CI Loops**: Version bump commits are detected and skipped @@ -115,6 +156,8 @@ When a PR is merged to `main`: 3. **Semantic Versioning**: Automated adherence to semver rules 4. **Traceability**: Git tags and changelog reflect all changes 5. **Safe Releases**: Tests run before version bump and deployment +6. **Fast CI**: SQLite tests run quickly without database server setup +7. **Comprehensive Testing**: Both unit and acceptance tests in PR workflow ## Future Enhancements diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml index 9689188..14c98ec 100644 --- a/.gitea/workflows/pr.yml +++ b/.gitea/workflows/pr.yml @@ -6,8 +6,65 @@ on: - main jobs: + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm run test:run + + acceptance-tests: + runs-on: ubuntu-latest + needs: unit-tests + env: + DATABASE_PROVIDER: sqlite + DATABASE_URL: file:./prisma/ci.db + BETTER_AUTH_SECRET: test-secret-key-for-ci-only + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npx prisma generate + + - name: Setup SQLite database + run: | + # Create SQLite database file + mkdir -p prisma + npx prisma migrate deploy + + - name: Run acceptance tests + run: npm run test:acceptance + env: + DATABASE_PROVIDER: sqlite + DATABASE_URL: file:./prisma/ci.db + BETTER_AUTH_SECRET: test-secret-key-for-ci-only + analyze-bump-type: runs-on: ubuntu-latest + needs: acceptance-tests steps: - name: Checkout code diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 1455602..4b9b4ea 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -4,11 +4,12 @@ on: push: branches: - '**' - - '!main' # Skip main branch (release workflow handles that) jobs: unit-tests: runs-on: ubuntu-latest + # Skip if this is an auto-generated version bump commit (handled by release workflow) + if: "!contains(github.event.head_commit.message, 'chore: bump version')" steps: - name: Checkout code -- 2.52.0 From 9980021037d196b5458836d0e94d2e01909596ab Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:48:47 -0700 Subject: [PATCH 07/17] feat: support both SQLite and PostgreSQL in prisma.ts --- src/lib/prisma.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 955900d..c906801 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,6 +1,4 @@ import { PrismaClient } from '@prisma/client' -import { PrismaPg } from '@prisma/adapter-pg' -import pg from 'pg' import dotenv from 'dotenv' // Load .env file if it exists @@ -10,11 +8,24 @@ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }) +// Detect database provider from environment (default to sqlite for local development) +const databaseProvider = process.env.DATABASE_PROVIDER || 'sqlite' -// Create PrismaClient with adapter +// Create PrismaClient with appropriate adapter const createPrismaClient = () => { - const client = new PrismaClient({ adapter }) + let client: PrismaClient + + if (databaseProvider === 'postgresql') { + // Use PrismaPg adapter for PostgreSQL + const { PrismaPg } = require('@prisma/adapter-pg') + const pg = require('pg') + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }) + client = new PrismaClient({ adapter }) + } else { + // No adapter needed for SQLite + client = new PrismaClient() + } + return client } -- 2.52.0 From e08d72bfe6de8405c4704b0b7aac52ea24878bdd Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:48:52 -0700 Subject: [PATCH 08/17] chore: update justfile with SQLite acceptance tests and utility tasks --- justfile | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/justfile b/justfile index 09b06f5..25cfe09 100644 --- a/justfile +++ b/justfile @@ -9,8 +9,8 @@ set positional-arguments # Project name PROJECT := "euchre-camp" -# Docker registry (CasaOS local registry) -REGISTRY := "euchre-camp" # Update to your registry host if needed +# Docker registry +REGISTRY := "docker.notsosm.art" IMAGE_TAG := "latest" # Get git commit hash (short version) @@ -22,6 +22,8 @@ IMAGE_TAG_COMMIT := `git rev-parse --short HEAD` # --- Variables --- # Database DB_CONTAINER := "euchre-camp-postgres" +DATABASE_PROVIDER := env_var_or_default("DATABASE_PROVIDER", "sqlite") +DATABASE_URL := env_var_or_default("DATABASE_URL", "file:./prisma/dev.db") # --- Setup & Installation --- @@ -54,15 +56,23 @@ format: # --- Testing --- -# Run all tests (unit + acceptance) -test: test-unit test-acceptance +# Run all tests (unit + acceptance with SQLite) +test: test-unit test-acceptance-sqlite + +# Run all tests with PostgreSQL (Docker) +test-pg: test-unit test-acceptance-postgres # Run unit tests (Vitest) test-unit: npm run test:run -# Run acceptance tests (Playwright) -test-acceptance: +# Run acceptance tests with SQLite (fast, no Docker needed) +test-acceptance-sqlite: + @echo "Running acceptance tests with SQLite..." + DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db BETTER_AUTH_SECRET=test-secret-key npm run test:acceptance + +# Run acceptance tests with PostgreSQL (Docker) +test-acceptance-postgres: @echo "Starting Docker containers for acceptance tests..." docker compose up -d @echo "Waiting for services to be ready..." @@ -80,6 +90,13 @@ migrate: seed: npm run db:seed +# Switch database provider +db-switch-sqlite: + npm run db:switch sqlite + +db-switch-postgres: + npm run db:switch postgresql + # --- Docker --- # Build the Docker image (standard build) @@ -143,9 +160,18 @@ docker-push: docker-build-full # --- CI/CD Pipeline Simulation --- # Run full CI pipeline locally (lint, test, build, push) -ci: lint typecheck test-unit docker-build +# Matches the Gitea Actions workflow +ci: lint typecheck test-unit test-acceptance-sqlite docker-build @echo "CI Pipeline completed successfully!" +# PR validation (what runs on pull requests) +pr-validate: lint typecheck test-unit test-acceptance-sqlite + @echo "PR validation completed successfully!" + +# Run CI with PostgreSQL (for release workflow simulation) +ci-postgres: lint typecheck test-unit test-acceptance-postgres docker-build + @echo "CI Pipeline with PostgreSQL completed successfully!" + # --- Utilities --- # Show help information @@ -158,3 +184,72 @@ clean: rm -rf node_modules .next dist @echo "Cleaning Docker artifacts..." docker system prune -f + +# Generate Prisma client +prisma-generate: + npx prisma generate + +# Reset development database +db-reset-dev: + npm run db:reset-dev + +# Setup development database +db-setup-dev: + npm run db:setup-dev + +# Clean production database (remove test records) +db-clean-prod: + npm run db:cleanup-prod + +# Check production database for test records +db-check-prod: + npm run db:check-prod + +# Create admin user +admin-create: + node scripts/create-admin-via-api.js + +# List all users +users-list: + node scripts/list-users.js + +# Update admin password +admin-update-password: + node scripts/update-admin-password.js + +# Bump version +version-bump-patch: + npm run version:patch + +version-bump-minor: + npm run version:minor + +version-bump-major: + npm run version:major + +# Docker Compose shortcuts +docker-up: + npm run docker:up + +docker-down: + npm run docker:down + +docker-logs: + npm run docker:logs + +# View workflow status +workflow-status: + @echo "Current workflows in .gitea/workflows/:" + ls -la .gitea/workflows/ + @echo "" + @echo "PR Workflow: Runs unit + acceptance tests on pull requests" + @echo "Test Workflow: Runs unit tests on all branch pushes" + @echo "Release Workflow: Runs on main branch pushes (version bump + Docker build)" + +# Check current database provider +db-status: + @echo "Database Provider: ${DATABASE_PROVIDER}" + @echo "Database URL: ${DATABASE_URL}" + @echo "" + @echo "Current schema.prisma provider:" + grep -A 2 "datasource db" prisma/schema.prisma | head -3 -- 2.52.0 From 5f02726ba605e6964d8c4bee735ab58f0f4ae9fd Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:48:58 -0700 Subject: [PATCH 09/17] docs: update README and AGENTS.md with CI/CD and justfile info --- AGENTS.md | 44 +++++++++++++++++++++++++++ README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7dd4506..464ab1c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,10 +109,50 @@ npm run db:setup-postgres **Note:** The database provider is automatically detected by Better Auth and Prisma. ### Running Tests + +**Using just (recommended):** +- **All tests**: `just test` (unit + acceptance with SQLite) +- **Unit tests**: `just test-unit` +- **Acceptance tests (SQLite)**: `just test-acceptance-sqlite` +- **Acceptance tests (PostgreSQL)**: `just test-acceptance-postgres` +- **PR validation**: `just pr-validate` (what runs on pull requests) + +**Using npm scripts:** - **Unit tests**: `npm run test` - **Acceptance tests**: `npm run test:acceptance` - **Specific test**: `npm run test:acceptance -- --grep "test name"` +**CI-style acceptance tests with SQLite:** +```bash +DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance +``` + +### CI/CD Pipeline +The project uses Gitea Actions for continuous integration: + +**PR Workflow** (`.gitea/workflows/pr.yml`): +- Runs on pull requests to main +- Executes unit tests and acceptance tests with SQLite +- Analyzes commits for semantic versioning +- Comments suggested bump type on PRs + +**Test Workflow** (`.gitea/workflows/test.yml`): +- Runs on all branch pushes +- Executes unit tests for quick feedback +- Skips auto-generated version bump commits + +**Release Workflow** (`.gitea/workflows/release.yml`): +- Runs on main branch pushes +- Determines version bump type +- Bumps version and creates git tags +- Builds Docker images and runs tests +- Pushes to registry and deploys + +**Database Strategy**: +- CI tests use SQLite (fast, no server required) +- Production uses PostgreSQL +- Switch with `DATABASE_PROVIDER` environment variable + ## Key Files ### Configuration @@ -170,6 +210,10 @@ npm run db:setup-postgres - Utilities: camelCase (e.g., `elo-utils.ts`) - Tests: `.test.ts` or `.test.tsx` suffix +## File Organization + +See [docs/FILE_ORGANIZATION.md](docs/FILE_ORGANIZATION.md) for detailed file organization and structure. + ## Resources - **Better Auth Docs**: https://better-auth.com/docs diff --git a/README.md b/README.md index 9ea63ac..2dd2f42 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,29 @@ EuchreCamp is a full-stack web application built with Next.js 14+ and TypeScript - **Framework**: Next.js 14+ (App Router) - **Language**: TypeScript -- **Database**: Prisma ORM with SQLite +- **Database**: Prisma ORM with SQLite (default) or PostgreSQL - **Styling**: Tailwind CSS - **Authentication**: Better Auth - **Form Handling**: React Hook Form + Zod validation - **CSV Parsing**: PapaParse - **Unit Testing**: Vitest - **Acceptance Testing**: Playwright +- **CI/CD**: Gitea Actions with SQLite for CI tests ## Project Structure ``` euchre_camp/ -├── src/ -│ ├── app/ +├── .gitea/workflows/ # CI/CD workflows (Gitea Actions) +├── docs/ # Documentation +│ ├── deployment/ # Deployment guides +│ └── planning/ # Planning documents +├── prisma/ # Prisma schema and migrations +├── public/ # Static assets +├── scripts/ # Utility scripts +│ └── python/ # Python scripts (legacy) +├── src/ # Source code +│ ├── app/ # Next.js app directory │ │ ├── api/ # API routes │ │ ├── auth/ # Authentication pages │ │ ├── admin/ # Admin pages @@ -39,16 +48,15 @@ euchre_camp/ │ │ └── components/ # Shared components │ ├── lib/ # Utilities and configuration │ │ ├── auth.ts # Better Auth configuration -│ │ ├── prisma.ts # Prisma client +│ │ ├── prisma.ts # Prisma client (SQLite/PostgreSQL) │ │ ├── permissions.ts # Authorization functions │ │ └── elo-utils.ts # Elo calculation utilities │ └── __tests__/ # Vitest and Playwright tests -├── prisma/ # Prisma schema and migrations -├── docs/ # Documentation -├── scripts/ # Utility scripts -└── public/ # Static assets +└── ... # Configuration files in root ``` +See [docs/FILE_ORGANIZATION.md](docs/FILE_ORGANIZATION.md) for detailed file organization. + ## Features Implemented ### Epic 1: Authentication & User Management @@ -242,6 +250,36 @@ Visit `/` to see: ## Development +### Using just (recommended) +The project includes a `justfile` with common development tasks: + +```bash +# Show all available tasks +just help + +# Development mode +just dev + +# Run all tests (unit + acceptance with SQLite) +just test + +# Run PR validation (what runs on pull requests) +just pr-validate + +# Run CI pipeline locally +just ci + +# Switch database provider +just db-switch-sqlite +just db-switch-postgres + +# Docker shortcuts +just docker-up +just docker-down +just docker-logs +``` + +### Using npm scripts directly ```bash # Development mode npm run dev @@ -255,6 +293,9 @@ npm run test # Run acceptance tests npm run test:acceptance + +# Run acceptance tests with SQLite (CI-style) +DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance ``` ### Database Commands @@ -313,6 +354,39 @@ User stories are organized into epics in `docs/USER_STORIES.md`: 7. Mobile Responsiveness 8. Data Management & Export +## CI/CD Pipeline + +The application uses Gitea Actions for continuous integration and deployment: + +### Workflow Architecture +1. **PR Workflow** (`.gitea/workflows/pr.yml`): Runs on pull requests + - Unit tests (fast feedback) + - Acceptance tests with SQLite database + - Semantic version bump analysis + +2. **Test Workflow** (`.gitea/workflows/test.yml`): Runs on all branch pushes + - Unit tests for quick feedback + - Skips auto-generated version bumps + +3. **Release Workflow** (`.gitea/workflows/release.yml`): Runs on main branch pushes + - Version bumping and tagging + - Docker image building and testing + - Registry push and deployment + +### Database Strategy for CI +- **CI Tests**: SQLite database (fast, no server required) +- **Production**: PostgreSQL (production-like environment) +- **Configuration**: `DATABASE_PROVIDER` environment variable + +### Running CI Locally +```bash +# Run unit tests (same as CI) +npm run test:run + +# Run acceptance tests with SQLite +DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance +``` + ## Docker Deployment This application can be run using Docker and Docker Compose. See [DOCKER.md](DOCKER.md) for detailed instructions. -- 2.52.0 From 0e1046fcb5f9a1c83de18d9f60f23d75dfb3f2db Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:49:07 -0700 Subject: [PATCH 10/17] docs: add FILE_ORGANIZATION.md to document project structure --- docs/FILE_ORGANIZATION.md | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/FILE_ORGANIZATION.md diff --git a/docs/FILE_ORGANIZATION.md b/docs/FILE_ORGANIZATION.md new file mode 100644 index 0000000..179e3fe --- /dev/null +++ b/docs/FILE_ORGANIZATION.md @@ -0,0 +1,125 @@ +# File Organization + +This document describes the organization of files in the EuchreCamp project. + +## Root Directory + +### Essential Files (Keep in Root) +- `README.md` - Main project documentation +- `AGENTS.md` - AI agent guide +- `CHANGELOG.md` - Version changelog +- `package.json` - Node.js dependencies and scripts +- `package-lock.json` - Dependency lock file +- `tsconfig.json` - TypeScript configuration +- `next.config.js` - Next.js configuration +- `.gitignore` - Git ignore file +- `Dockerfile` - Docker build configuration +- `justfile` - Development task automation + +### Configuration Files (Keep in Root) +- `.eslintrc.json` - ESLint configuration +- `postcss.config.mjs` - PostCSS configuration +- `playwright.config.ts` - Playwright test configuration +- `vitest.config.mts` - Vitest configuration +- `vitest.setup.ts` - Vitest setup +- `.dockerignore` - Docker ignore file +- `mise.toml` - Mise version manager config + +### Docker Files (Keep in Root) +- `docker-compose.yml` - Main Docker Compose +- `docker-compose.dev.yml` - Development Docker Compose +- `docker-compose.override.yml` - Override for dev +- `docker-compose.casaos.yml` - CasaOS specific + +### Environment Files (Keep in Root, Gitignored) +- `.env` - Environment variables +- `.env.development` - Development environment + +## Organized Directories + +### `.gitea/` - Gitea Actions Workflows +- `workflows/pr.yml` - Pull request workflow (unit + acceptance tests) +- `workflows/test.yml` - Test workflow (unit tests on branch pushes) +- `workflows/release.yml` - Release workflow (version bump + Docker build) +- `WORKFLOW_ARCHITECTURE.md` - Workflow architecture documentation + +### `docs/` - Documentation +- `deployment/` - Deployment documentation + - `CASAOS_DEPLOYMENT.md` - CasaOS deployment guide + - `DOCKER.md` - Docker deployment instructions +- `TODO.md` - Project TODO list (in docs root for visibility) +- `USER_STORIES.md` - User stories organized by epic +- Other documentation files (design, implementation, testing, etc.) + +### `scripts/` - Utility Scripts +- `python/` - Python scripts (legacy/old functionality) + - `generate_games.py` - Generate sample games + - `update_partnership_stats.py` - Update partnership stats + - `update_player_stats.py` - Update player stats +- `bump-version.js` - Version bumping script +- `build-and-push-docker.js` - Docker build and push script +- `switch-database.js` - Database provider switching +- `create-admin-via-api.js` - Admin user creation via API +- `create-admin-better-auth.js` - Admin user creation via database +- `list-users.js` - List all users +- `update-admin-password.js` - Update admin password +- `seed.js` - Database seeding +- And other Node.js scripts... + +### `src/` - Source Code +- `app/` - Next.js app directory + - `api/` - API routes + - `auth/` - Authentication pages + - `admin/` - Admin pages + - `players/` - Player pages + - `rankings/` - Rankings page + - `components/` - Shared components +- `lib/` - Utilities and configuration + - `auth.ts` - Better Auth configuration + - `prisma.ts` - Prisma client (supports SQLite and PostgreSQL) + - `permissions.ts` - Authorization functions + - `elo-utils.ts` - Elo calculation utilities +- `__tests__/` - Vitest and Playwright tests + - `unit/` - Unit tests + - `e2e/` - End-to-end acceptance tests + +### `prisma/` - Database +- `schema.prisma` - Prisma schema +- `migrations/` - Database migrations +- `dev.db` - SQLite development database (if using SQLite) + +### `public/` - Static Assets +- Images, fonts, and other static files + +### `playwright/` - Playwright Test Data +- Authentication state files + +## Generated Directories (Gitignored) +- `.next/` - Next.js build output +- `node_modules/` - Node.js dependencies +- `playwright-report/` - Playwright test reports +- `test-results/` - Test results + +## File Organization Principles + +1. **Keep standard files in root**: package.json, tsconfig.json, etc. +2. **Organize by function**: Group related files in directories +3. **Separate generated from source**: Keep build outputs and dependencies separate +4. **Document organization**: Use this file to explain structure +5. **Follow conventions**: Use standard naming and organization patterns + +## CI/CD File Organization + +### Workflows +- `.gitea/workflows/pr.yml` - Pull request validation +- `.gitea/workflows/test.yml` - Branch testing +- `.gitea/workflows/release.yml` - Main branch release + +### Database Strategy +- **CI/Testing**: SQLite (fast, no server) +- **Production**: PostgreSQL (production-like) + +### Testing +- Unit tests: `npm run test:run` +- Acceptance tests (SQLite): `DATABASE_PROVIDER=sqlite npm run test:acceptance` +- Acceptance tests (PostgreSQL): `npm run test:acceptance` (with Docker) -- 2.52.0 From 9511eed477edc246f2b8f4f62ea7e37dcddf6ce0 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:49:12 -0700 Subject: [PATCH 11/17] refactor: move deployment docs to docs/deployment/ directory --- docs/deployment/CASAOS_DEPLOYMENT.md | 273 ++++++++++++++++++++ docs/deployment/DOCKER.md | 370 +++++++++++++++++++++++++++ 2 files changed, 643 insertions(+) create mode 100644 docs/deployment/CASAOS_DEPLOYMENT.md create mode 100644 docs/deployment/DOCKER.md diff --git a/docs/deployment/CASAOS_DEPLOYMENT.md b/docs/deployment/CASAOS_DEPLOYMENT.md new file mode 100644 index 0000000..2bcccfa --- /dev/null +++ b/docs/deployment/CASAOS_DEPLOYMENT.md @@ -0,0 +1,273 @@ +# EuchreCamp CasaOS Deployment Guide + +This guide explains how to deploy EuchreCamp on your CasaOS server. + +## Prerequisites + +1. **CasaOS** installed on your server +2. **Docker** and **Docker Compose** available in CasaOS +3. **Port 3000** available for the web interface +4. **Port 5432** available for PostgreSQL (or use external database) + +## Prerequisites + +**External PostgreSQL Database Required** +- PostgreSQL 15 or higher +- Database name: `euchre_camp` +- User: `euchre_camp` +- Privileges: ALL on database `euchre_camp` + +## Quick Start + +### Option 1: Using CasaOS App Store (Recommended) + +1. **Set up PostgreSQL database first** (see below) +2. Open CasaOS Dashboard +3. Go to **App Store** → **Custom App** +4. Click **Install** and configure the following: + + **Basic Settings:** + - App Name: `EuchreCamp` + - Image: `euchre-camp:latest` (build locally first) + - Port: `3000` + + **Environment Variables:** + ``` + DATABASE_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp + DATABASE_SHADOW_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp_shadow + BETTER_AUTH_SECRET=your-secret-key-here + BETTER_AUTH_URL=http://your-casaos-ip:3000 + TRUSTED_ORIGINS=http://your-casaos-ip:3000 + ``` + +5. Click **Install** + +### Option 2: Using Docker Compose + +1. **Set up PostgreSQL database first** (see below) +2. **Build the Docker image:** + ```bash + docker-compose -f docker-compose.casaos.yml build + ``` + +3. **Start the application:** + ```bash + docker-compose -f docker-compose.casaos.yml up -d + ``` + +4. **Check logs:** + ```bash + docker-compose -f docker-compose.casaos.yml logs -f + ``` + +## PostgreSQL Database Setup + +### Option A: External PostgreSQL Server + +Connect to your PostgreSQL server and run: + +```sql +-- Create database +CREATE DATABASE euchre_camp; + +-- Create user +CREATE USER euchre_camp WITH PASSWORD 'your-strong-password'; + +-- Grant privileges +GRANT ALL PRIVILEGES ON DATABASE euchre_camp TO euchre_camp; + +-- Connect to database +\c euchre_camp + +-- Grant schema privileges +GRANT ALL ON SCHEMA public TO euchre_camp; +``` + +### Option B: CasaOS PostgreSQL Container + +If you want to run PostgreSQL in CasaOS: + +1. Install PostgreSQL from CasaOS App Store +2. Configure it with: + - Database: `euchre_camp` + - User: `euchre_camp` + - Password: `your-strong-password` +3. Use the connection string in your EuchreCamp configuration: + ``` + DATABASE_URL=postgresql://euchre_camp:your-strong-password@192.168.1.100:5432/euchre_camp + ``` + (Replace `192.168.1.100` with your CasaOS IP) + +## Configuration + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `DATABASE_URL` | PostgreSQL connection string (REQUIRED) | `postgresql://euchre_camp:pass@host:5432/euchre_camp` | +| `BETTER_AUTH_SECRET` | Secret key for session encryption (generate with `openssl rand -base64 32`) | `your-generated-secret` | +| `BETTER_AUTH_URL` | Public URL of your application | `http://192.168.1.100:3000` | +| `TRUSTED_ORIGINS` | Comma-separated list of trusted origins | `http://192.168.1.100:3000` | + +### Optional Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `DATABASE_SHADOW_URL` | Shadow database for migrations | Same as DATABASE_URL with `_shadow` suffix | + +## Database Management + +### First-Time Setup + +1. **Create the database and user** (see PostgreSQL Setup above) +2. **Run database migrations:** + ```bash + docker exec -it euchre-camp npx prisma migrate deploy + ``` +3. **Create admin user:** + ```bash + docker exec -it euchre-camp node scripts/create-admin-via-api.js + ``` + +### Accessing the Database + +```bash +# Connect to external PostgreSQL server +psql -h your-postgres-host -U euchre_camp -d euchre_camp + +# List tables +\dt + +# Exit +\q +``` + +### Backing Up Data + +```bash +# Backup PostgreSQL data (run on your PostgreSQL server) +pg_dump -h your-postgres-host -U euchre_camp euchre_camp > backup.sql + +# Backup uploaded files +tar -czf euchre_camp_files.tar.gz /var/lib/docker/volumes/euchre_camp_app_data +``` + +## Security Considerations + +### Change Default Passwords + +1. **PostgreSQL Password:** + - Edit `docker-compose.casaos.yml` or set via CasaOS environment variables + - Update `POSTGRES_PASSWORD` to a strong password + +2. **Better Auth Secret:** + ```bash + openssl rand -base64 32 + ``` + - Use this value for `BETTER_AUTH_SECRET` + +### HTTPS Setup + +For production use with HTTPS: + +1. **Option A: CasaOS Reverse Proxy** + - Configure CasaOS to use HTTPS + - Update `BETTER_AUTH_URL` to `https://your-domain.com` + +2. **Option B: External Reverse Proxy (Nginx, Traefik)** + - Configure your reverse proxy + - Update `TRUSTED_ORIGINS` to include your domain + +### Firewall Rules + +Ensure only necessary ports are exposed: +- **Port 3000**: Web interface (HTTP/HTTPS) +- **Port 5432**: PostgreSQL (only if external access needed) + +## Troubleshooting + +### Database Connection Issues + +```bash +# Check PostgreSQL logs +docker logs euchre-camp-postgres + +# Check if PostgreSQL is healthy +docker exec euchre-camp-postgres pg_isready -U euchre -d euchre_camp +``` + +### Application Not Starting + +```bash +# Check application logs +docker logs euchre-camp + +# Check if Prisma migrations are applied +docker exec euchre-camp npx prisma migrate status +``` + +### Permission Issues + +If you see permission errors: +```bash +# Fix volume permissions +docker exec euchre-camp chown -R euchre:euchre /app/public/uploads +``` + +## Performance Tuning + +### PostgreSQL Optimization + +Add to `docker-compose.casaos.yml` under `postgres` service: +```yaml +environment: + - POSTGRES_SHARED_BUFFERS=256MB + - POSTGRES_WORK_MEM=16MB +``` + +### Application Optimization + +The Dockerfile is already optimized with: +- Multi-stage build (smaller image size) +- Non-root user for security +- Health checks +- Production-ready settings + +## Updating + +To update EuchreCamp: + +```bash +# Pull latest changes +git pull origin main + +# Rebuild and restart +docker-compose -f docker-compose.casaos.yml down +docker-compose -f docker-compose.casaos.yml build --no-cache +docker-compose -f docker-compose.casaos.yml up -d +``` + +## Monitoring + +### View Logs +```bash +docker-compose -f docker-compose.casaos.yml logs -f +``` + +### Check Container Status +```bash +docker ps | grep euchre +``` + +### Resource Usage +```bash +docker stats euchre-camp euchre-camp-postgres +``` + +## Support + +For issues or questions: +- Check the application logs first +- Verify environment variables are set correctly +- Ensure PostgreSQL is running and accessible +- Check CasaOS app logs for deployment issues diff --git a/docs/deployment/DOCKER.md b/docs/deployment/DOCKER.md new file mode 100644 index 0000000..9f4ea0f --- /dev/null +++ b/docs/deployment/DOCKER.md @@ -0,0 +1,370 @@ +# Docker Deployment Guide + +This guide covers how to run EuchreCamp using Docker and Docker Compose. + +## Prerequisites + +- Docker Engine 20.10+ +- Docker Compose v2.0+ +- Make (optional, for using Makefile commands) + +## Quick Start + +### 1. Environment Setup + +Create a `.env` file in the project root: + +```bash +cp .env.example .env +``` + +Update the `.env` file with your configuration: + +```env +# Database Configuration (PostgreSQL in Docker) +DATABASE_PROVIDER=postgresql +DATABASE_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp +DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp_shadow + +# Better Auth (generate a secure secret for production) +BETTER_AUTH_SECRET=your-secret-key-change-in-production +BETTER_AUTH_URL=http://localhost:3000 + +# Application +NODE_ENV=production +TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 +``` + +### 2. Build and Run + +#### Production Mode + +```bash +# Build and start all services +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop all services +docker-compose down +``` + +#### Development Mode + +```bash +# Use development override configuration +docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d + +# Or use the shorthand +docker-compose --profile dev up -d +``` + +### 3. Access the Application + +- **Web Application**: http://localhost:3000 +- **PostgreSQL**: localhost:5432 (default) or 5433 in development + +## Service Architecture + +``` +┌─────────────────────────────────────────────┐ +│ EuchreCamp │ +├─────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ │ +│ │ Next.js │◄───│ PostgreSQL │ │ +│ │ App │ │ Database │ │ +│ │ │ │ │ │ +│ │ Port: 3000 │ │ Port: 5432 │ │ +│ └──────────────┘ └──────────────────┘ │ +│ │ +└─────────────────────────────────────────────┘ +``` + +## Service Details + +### App Service + +- **Image**: Multi-stage Next.js build +- **Port**: 3000 +- **Environment**: Production or Development +- **Volumes**: + - `app_data`: Persistent storage for uploads + +### PostgreSQL Service + +- **Image**: postgres:15-alpine +- **Port**: 5432 (production) / 5433 (development) +- **Data Volume**: `postgres_data` (persistent) +- **Health Check**: Uses `pg_isready` + +## Database Management + +### View Database Data + +```bash +# Access PostgreSQL shell +docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp + +# List tables +\dt + +# Exit +\q +``` + +### Backup Database + +```bash +# Create backup +docker exec euchre-camp-postgres pg_dump -U euchre euchre_camp > backup.sql + +# Restore backup +docker exec -i euchre-camp-postgres psql -U euchre euchre_camp < backup.sql +``` + +### Reset Database + +```bash +# Remove all data and restart +docker-compose down -v +docker-compose up -d +``` + +## Running Commands + +### Run Database Migrations + +```bash +# Apply migrations +docker-compose exec app npx prisma migrate deploy + +# Generate Prisma client +docker-compose exec app npx prisma generate + +# Open Prisma Studio +docker-compose exec app npx prisma studio +``` + +### Create Admin User + +```bash +# Create admin user via script +docker-compose exec app node scripts/create-admin-via-api.js +``` + +### Seed Database + +```bash +# Run seed script +docker-compose exec app node scripts/seed.js +``` + +## Development Workflow + +### Hot Reloading + +Development mode supports hot reloading: + +```bash +# Start in development mode +docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d + +# View logs +docker-compose logs -f app +``` + +### Modifying Code + +Changes to source code are automatically reflected due to volume mounting: + +```bash +# Edit a file locally +nano src/app/page.tsx + +# Next.js will automatically reload +``` + +## Troubleshooting + +### Common Issues + +**1. Database Connection Failed** + +```bash +# Check if PostgreSQL is running +docker ps | grep postgres + +# Check logs +docker logs euchre-camp-postgres + +# Restart services +docker-compose restart postgres +``` + +**2. Port Already in Use** + +```bash +# Check what's using port 3000 +lsof -i :3000 + +# Change port in docker-compose.yml +# ports: +# - "3001:3000" +``` + +**3. Permission Denied** + +```bash +# Ensure scripts are executable +chmod +x scripts/*.sh + +# Check volume permissions +docker exec euchre-camp-app ls -la /app +``` + +**4. Migration Errors** + +```bash +# Reset database +docker-compose down -v +docker-compose up -d + +# Apply migrations manually +docker-compose exec app npx prisma migrate deploy +``` + +### View Logs + +```bash +# All services +docker-compose logs -f + +# Specific service +docker-compose logs -f app +docker-compose logs -f postgres +``` + +### Container Shell Access + +```bash +# App container +docker exec -it euchre-camp-app sh + +# PostgreSQL container +docker exec -it euchre-camp-postgres sh +``` + +## Production Deployment + +### Environment Variables + +For production, ensure you set secure values: + +```env +# Generate a secure secret +BETTER_AUTH_SECRET=$(openssl rand -base64 32) + +# Use real domain +BETTER_AUTH_URL=https://euchrecamp.example.com + +# Trusted origins +TRUSTED_ORIGINS=https://euchrecamp.example.com,https://www.euchrecamp.example.com +``` + +### Security Considerations + +1. **Change default PostgreSQL password**: + ```env + POSTGRES_PASSWORD=your-secure-password + ``` + +2. **Use SSL/TLS for database connections**: + ```env + DATABASE_URL=postgresql://euchre:password@postgres:5432/euchre_camp?sslmode=require + ``` + +3. **Restrict network access**: + - Don't expose PostgreSQL port to public internet + - Use firewall rules to restrict access + +4. **Regular backups**: + - Schedule automated database backups + - Test restoration process + +### Docker Swarm / Kubernetes + +For production orchestration, consider: + +1. **Separate database service** - Use managed PostgreSQL (RDS, Cloud SQL) +2. **Secrets management** - Use Docker secrets or Kubernetes Secrets +3. **Health checks** - Configure proper health checks +4. **Resource limits** - Set CPU and memory limits +5. **Replicas** - Run multiple app instances for high availability + +## Makefile (Optional) + +Create a `Makefile` for convenient commands: + +```makefile +.PHONY: up down logs build shell-migrate + +up: + docker-compose up -d + +down: + docker-compose down + +logs: + docker-compose logs -f + +build: + docker-compose build --no-cache + +shell-app: + docker exec -it euchre-camp-app sh + +shell-db: + docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp + +migrate: + docker-compose exec app npx prisma migrate deploy + +seed: + docker-compose exec app node scripts/seed.js +``` + +## Performance Tuning + +### PostgreSQL Configuration + +Edit `docker-compose.yml` to tune PostgreSQL: + +```yaml +postgres: + # ... other config ... + command: > + postgres + -c max_connections=200 + -c shared_buffers=256MB + -c effective_cache_size=1GB + -c maintenance_work_mem=64MB + -c checkpoint_completion_target=0.9 + -c wal_buffers=16MB + -c default_statistics_target=100 +``` + +### Next.js Optimization + +The Dockerfile uses multi-stage build for optimal size and performance: + +- **Builder stage**: Installs all dependencies and builds the app +- **Runner stage**: Copies only the built artifacts and production dependencies + +## References + +- [Docker Compose Documentation](https://docs.docker.com/compose/) +- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres) +- [Next.js Deployment](https://nextjs.org/docs/deployment) +- [Prisma Deployment](https://www.prisma.io/docs/concepts/components/prisma-migrate/deployment) -- 2.52.0 From 039b927bb78be80e4f6055c38598016a6b61cc70 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:49:20 -0700 Subject: [PATCH 12/17] refactor: move Python scripts to scripts/python/ directory --- scripts/python/generate_games.py | 138 ++++++++++++ scripts/python/update_partnership_stats.py | 234 +++++++++++++++++++++ scripts/python/update_player_stats.py | 195 +++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 scripts/python/generate_games.py create mode 100644 scripts/python/update_partnership_stats.py create mode 100644 scripts/python/update_player_stats.py diff --git a/scripts/python/generate_games.py b/scripts/python/generate_games.py new file mode 100644 index 0000000..df38ad6 --- /dev/null +++ b/scripts/python/generate_games.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Generate 100+ games to test ELO rating calculations +""" + +import sqlite3 +import random +from datetime import datetime, timedelta + +DB_PATH = "prisma/prisma/dev.db" + + +def get_players(): + """Get all players from the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT id, name, currentElo FROM players") + players = cursor.fetchall() + conn.close() + return players + + +def generate_game(players, game_num, base_date): + """Generate a single game with random players and scores""" + # Randomly select 4 different players + selected_players = random.sample(players, 4) + p1, p2, p3, p4 = selected_players + + # Randomly assign teams (Team 1 vs Team 2) + team1_p1 = p1 + team1_p2 = p2 + team2_p1 = p3 + team2_p2 = p4 + + # Generate realistic scores (Euchre games typically 10 points max) + # Team 1 wins 60% of the time for variety + team1_wins = random.random() < 0.6 + if team1_wins: + # Team 1 wins - generate scores + team1_score = random.randint(10, 15) + team2_score = random.randint(0, 9) + else: + # Team 2 wins - generate scores + team2_score = random.randint(10, 15) + team1_score = random.randint(0, 9) + + # Generate random date in the past 30 days + days_ago = random.randint(0, 30) + game_date = base_date - timedelta( + days=days_ago, hours=random.randint(0, 23), minutes=random.randint(0, 59) + ) + + return { + "team1P1Id": team1_p1[0], + "team1P2Id": team1_p2[0], + "team2P1Id": team2_p1[0], + "team2P2Id": team2_p2[0], + "team1Score": team1_score, + "team2Score": team2_score, + "playedAt": game_date.isoformat() + "Z", + "status": "completed", + "eventId": None, + } + + +def insert_games(games): + """Insert games into the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + for game in games: + cursor.execute( + """ + INSERT INTO matches + (team1P1Id, team1P2Id, team2P1Id, team2P2Id, team1Score, team2Score, playedAt, status, eventId, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + game["team1P1Id"], + game["team1P2Id"], + game["team2P1Id"], + game["team2P2Id"], + game["team1Score"], + game["team2Score"], + game["playedAt"], + game["status"], + game["eventId"], + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + ), + ) + + conn.commit() + conn.close() + + +def main(): + print("Generating 150 games to test ELO ratings...") + print("=" * 60) + + players = get_players() + print(f"Found {len(players)} players in database") + + # Generate 150 games + num_games = 150 + base_date = datetime.now() + + games = [] + for i in range(num_games): + game = generate_game(players, i, base_date) + games.append(game) + + print(f"Generated {len(games)} games") + + # Insert games into database + print("Inserting games into database...") + insert_games(games) + print("Games inserted successfully!") + + # Show sample games + print("\nSample games:") + print("-" * 80) + for i, game in enumerate(games[:3]): + print( + f"Game {i + 1}: Team 1 ({game['team1Score']}) vs Team 2 ({game['team2Score']})" + ) + print(f" Team 1: Player {game['team1P1Id']} + Player {game['team1P2Id']}") + print(f" Team 2: Player {game['team2P1Id']} + Player {game['team2P2Id']}") + print(f" Date: {game['playedAt']}") + print() + + print("=" * 60) + print(f"Total games generated: {num_games}") + print("Now check the ELO ratings by running the application!") + + +if __name__ == "__main__": + main() diff --git a/scripts/python/update_partnership_stats.py b/scripts/python/update_partnership_stats.py new file mode 100644 index 0000000..7d9f82a --- /dev/null +++ b/scripts/python/update_partnership_stats.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Update partnership statistics based on matches in the database +""" + +import sqlite3 +import math +from datetime import datetime + +DB_PATH = "prisma/prisma/dev.db" +K_FACTOR = 32 + + +def get_all_matches(): + """Get all matches from the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, + team1Score, team2Score, playedAt + FROM matches + ORDER BY playedAt + """) + matches = cursor.fetchall() + conn.close() + return matches + + +def get_or_create_partnership(player1_id, player2_id): + """Get or create a partnership record""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Sort IDs to ensure consistent partnership lookup + p1 = min(player1_id, player2_id) + p2 = max(player1_id, player2_id) + + cursor.execute( + """ + SELECT id, gamesPlayed, wins, losses, totalEloChange + FROM partnership_stats + WHERE player1Id = ? AND player2Id = ? + """, + (p1, p2), + ) + + result = cursor.fetchone() + if result: + conn.close() + return result + + # Create new partnership record + cursor.execute( + """ + INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) + VALUES (?, ?, 0, 0, 0, 0, NULL, ?, ?) + """, + (p1, p2, datetime.now().isoformat() + "Z", datetime.now().isoformat() + "Z"), + ) + + partnership_id = cursor.lastrowid + conn.commit() + conn.close() + + return (partnership_id, 0, 0, 0, 0) + + +def update_partnership_stats(player1_id, player2_id, won, elo_change): + """Update partnership statistics""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Sort IDs to ensure consistent partnership lookup + p1 = min(player1_id, player2_id) + p2 = max(player1_id, player2_id) + + # Get current partnership stats + cursor.execute( + """ + SELECT id, gamesPlayed, wins, losses, totalEloChange + FROM partnership_stats + WHERE player1Id = ? AND player2Id = ? + """, + (p1, p2), + ) + + result = cursor.fetchone() + if not result: + # Create new partnership record + cursor.execute( + """ + INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) + VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?) + """, + ( + p1, + p2, + 1 if won else 0, + 0 if won else 1, + elo_change, + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + ), + ) + else: + partnership_id, games_played, wins, losses, total_elo_change = result + + # Update partnership stats + new_games = games_played + 1 + new_wins = wins + 1 if won else wins + new_losses = losses if won else losses + 1 + new_total_elo_change = total_elo_change + elo_change + + cursor.execute( + """ + UPDATE partnership_stats + SET gamesPlayed = ?, wins = ?, losses = ?, totalEloChange = ?, lastPlayed = ?, updatedAt = ? + WHERE id = ? + """, + ( + new_games, + new_wins, + new_losses, + new_total_elo_change, + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + partnership_id, + ), + ) + + conn.commit() + conn.close() + + +def main(): + print("Updating partnership statistics based on matches...") + print("=" * 60) + + # Get all matches + matches = get_all_matches() + print(f"Found {len(matches)} matches in database") + + # Reset partnership stats + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("DELETE FROM partnership_stats") + conn.commit() + conn.close() + print("Reset all partnership statistics") + + # Process each match + match_count = 0 + for ( + match_id, + team1_p1, + team1_p2, + team2_p1, + team2_p2, + team1_score, + team2_score, + played_at, + ) in matches: + # Determine winners + team1_won = team1_score > team2_score + team2_won = team2_score > team1_score + + # Update partnership stats for Team 1 + if team1_won: + update_partnership_stats( + team1_p1, team1_p2, True, 0 + ) # Elo change will be calculated separately + else: + update_partnership_stats(team1_p1, team1_p2, False, 0) + + # Update partnership stats for Team 2 + if team2_won: + update_partnership_stats(team2_p1, team2_p2, True, 0) + else: + update_partnership_stats(team2_p1, team2_p2, False, 0) + + match_count += 1 + if match_count % 20 == 0: + print(f"Processed {match_count}/{len(matches)} matches...") + + print(f"Processed {match_count} matches") + + # Display partnership stats for top players + print("\n" + "=" * 60) + print("Partnership Stats for Top Players:") + print("-" * 60) + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Get top players by ELO + cursor.execute("SELECT id, name FROM players ORDER BY currentElo DESC LIMIT 5") + top_players = cursor.fetchall() + + for player_id, player_name in top_players: + print(f"\n{player_name}:") + + # Get partnership stats for this player + cursor.execute( + """ + SELECT + CASE + WHEN player1Id = ? THEN (SELECT name FROM players WHERE id = player2Id) + ELSE (SELECT name FROM players WHERE id = player1Id) + END as partner_name, + gamesPlayed, wins, losses, totalEloChange + FROM partnership_stats + WHERE player1Id = ? OR player2Id = ? + ORDER BY gamesPlayed DESC + LIMIT 3 + """, + (player_id, player_id, player_id), + ) + + partnerships = cursor.fetchall() + for partner_name, games, wins, losses, elo_change in partnerships: + win_rate = (wins / games * 100) if games > 0 else 0 + print( + f" - with {partner_name}: {games} games, {wins}/{losses} ({win_rate:.1f}%) ELO: {elo_change:+d}" + ) + + conn.close() + + print("\n" + "=" * 60) + print("Partnership statistics updated successfully!") + + +if __name__ == "__main__": + main() diff --git a/scripts/python/update_player_stats.py b/scripts/python/update_player_stats.py new file mode 100644 index 0000000..b2525da --- /dev/null +++ b/scripts/python/update_player_stats.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Update player statistics (ELO, gamesPlayed, wins, losses) based on matches in the database +""" + +import sqlite3 +import math + +DB_PATH = "prisma/prisma/dev.db" +K_FACTOR = 32 # Standard K-factor for Elo calculations + + +def get_all_matches(): + """Get all matches from the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, + team1Score, team2Score, playedAt + FROM matches + ORDER BY playedAt + """) + matches = cursor.fetchall() + conn.close() + return matches + + +def get_player(player_id): + """Get a player by ID""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + "SELECT id, name, currentElo, gamesPlayed, wins, losses FROM players WHERE id = ?", + (player_id,), + ) + player = cursor.fetchone() + conn.close() + return player + + +def update_player(player_id, current_elo, games_played, wins, losses): + """Update a player's statistics""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + """ + UPDATE players + SET currentElo = ?, gamesPlayed = ?, wins = ?, losses = ? + WHERE id = ? + """, + (current_elo, games_played, wins, losses, player_id), + ) + conn.commit() + conn.close() + + +def calculate_elo_change(rating_a, rating_b, score_a, score_b): + """Calculate Elo change for a match""" + # Calculate expected scores + expected_a = 1 / (1 + math.pow(10, (rating_b - rating_a) / 400)) + expected_b = 1 - expected_a + + # Actual scores (1 for win, 0.5 for tie, 0 for loss) + actual_a = 0.5 if score_a == score_b else (1 if score_a > score_b else 0) + actual_b = 0.5 if score_a == score_b else (1 if score_b > score_a else 0) + + # Calculate Elo change + elo_change_a = K_FACTOR * (actual_a - expected_a) + elo_change_b = K_FACTOR * (actual_b - expected_b) + + return elo_change_a, elo_change_b + + +def main(): + print("Updating player statistics based on matches in database...") + print("=" * 60) + + # Get all matches + matches = get_all_matches() + print(f"Found {len(matches)} matches in database") + + # Reset all player stats to 0 before recalculating + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + "UPDATE players SET currentElo = 1000, gamesPlayed = 0, wins = 0, losses = 0" + ) + conn.commit() + conn.close() + print("Reset all player statistics to initial values (ELO: 1000, games: 0)") + + # Process each match + match_count = 0 + for ( + match_id, + team1_p1, + team1_p2, + team2_p1, + team2_p2, + team1_score, + team2_score, + played_at, + ) in matches: + # Get player data + p1 = get_player(team1_p1) + p2 = get_player(team1_p2) + p3 = get_player(team2_p1) + p4 = get_player(team2_p2) + + if not all([p1, p2, p3, p4]): + print(f"Warning: Could not find all players for match {match_id}") + continue + + # Calculate team ratings + team1_rating = (p1[2] + p2[2]) / 2 # currentElo + team2_rating = (p3[2] + p4[2]) / 2 # currentElo + + # Calculate Elo changes + team1_elo_change, team2_elo_change = calculate_elo_change( + team1_rating, team2_rating, team1_score, team2_score + ) + + # Individual Elo changes (split evenly between team members) + p1_elo_change = team1_elo_change / 2 + p2_elo_change = team1_elo_change / 2 + p3_elo_change = team2_elo_change / 2 + p4_elo_change = team2_elo_change / 2 + + # Determine winners + team1_won = team1_score > team2_score + team2_won = team2_score > team1_score + + # Update player 1 stats + p1_new_elo = int(p1[2] + p1_elo_change) + p1_new_games = p1[3] + 1 + p1_new_wins = p1[4] + 1 if team1_won else p1[4] + p1_new_losses = p1[5] if team1_won else p1[5] + 1 + update_player(p1[0], p1_new_elo, p1_new_games, p1_new_wins, p1_new_losses) + + # Update player 2 stats + p2_new_elo = int(p2[2] + p2_elo_change) + p2_new_games = p2[3] + 1 + p2_new_wins = p2[4] + 1 if team1_won else p2[4] + p2_new_losses = p2[5] if team1_won else p2[5] + 1 + update_player(p2[0], p2_new_elo, p2_new_games, p2_new_wins, p2_new_losses) + + # Update player 3 stats + p3_new_elo = int(p3[2] + p3_elo_change) + p3_new_games = p3[3] + 1 + p3_new_wins = p3[4] + 1 if team2_won else p3[4] + p3_new_losses = p3[5] if team2_won else p3[5] + 1 + update_player(p3[0], p3_new_elo, p3_new_games, p3_new_wins, p3_new_losses) + + # Update player 4 stats + p4_new_elo = int(p4[2] + p4_elo_change) + p4_new_games = p4[3] + 1 + p4_new_wins = p4[4] + 1 if team2_won else p4[4] + p4_new_losses = p4[5] if team2_won else p4[5] + 1 + update_player(p4[0], p4_new_elo, p4_new_games, p4_new_wins, p4_new_losses) + + match_count += 1 + if match_count % 20 == 0: + print(f"Processed {match_count}/{len(matches)} matches...") + + print(f"Processed {match_count} matches") + + # Display updated player rankings + print("\n" + "=" * 60) + print("Top 10 Players by ELO Rating:") + print("-" * 60) + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT id, name, currentElo, gamesPlayed, wins, losses + FROM players + ORDER BY currentElo DESC + LIMIT 10 + """) + top_players = cursor.fetchall() + conn.close() + + for rank, (player_id, name, elo, games, wins, losses) in enumerate(top_players, 1): + win_rate = (wins / games * 100) if games > 0 else 0 + print( + f"{rank:2}. {name:15} | ELO: {elo:4} | Games: {games:3} | W/L: {wins}/{losses} ({win_rate:.1f}%)" + ) + + print("\n" + "=" * 60) + print("Player statistics updated successfully!") + print("Now run the application to see updated rankings.") + + +if __name__ == "__main__": + main() -- 2.52.0 From 80714a5daab8154313b2cf4639ce5c16fbad847a Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:49:26 -0700 Subject: [PATCH 13/17] refactor: move TODO.md to docs directory for better organization --- docs/TODO.md | 204 ++++++++++++++++++++++----------------------------- 1 file changed, 86 insertions(+), 118 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 7dc890b..b5529c0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,134 +1,102 @@ -# EuchreCamp - Project Todo List +# EuchreCamp - Todo List -## Completed Features +## Current Tasks -### Backend -- [x] Database schema for matches, players, teams, events -- [x] Elo rating calculator and job -- [x] Partnership tracking and analytics -- [x] Tournament generator (round-robin, single elim, double elim, Swiss) -- [x] ROM relations and repositories -- [x] Acceptance test suite (8 tests passing) +### Completed ✅ +- [x] Add `site_admin` role to database schema and permissions system +- [x] Add `isCasual` boolean field to Match model (already existed) +- [x] Update match upload API to support casual matches +- [x] Update match upload UI to include casual checkbox +- [x] Add tournament deletion API endpoint with delete/orphan options +- [x] Add delete tournament button and modal to tournament detail page +- [x] Run tests and verify implementation (84 tests passing) +- [x] Fix session issues with tournament admin access +- [x] Fix Elo recalculation error for player merge (delete elo snapshots before deleting players) +- [x] Add admin player management page +- [x] Add player name editing functionality in admin UI +- [x] Add admin panel links to navigation header +- [x] Add tournament update API endpoint (PUT /api/tournaments/[id]) +- [x] Consolidate delete endpoint from admin API to main tournaments API +- [x] Update database schema to add variant scoring fields (targetScore, allowTies) +- [x] Fix tie handling logic in partnership stats (ties now correctly tracked) +- [x] Fix test files for normalizedName field in Player model +- [x] Fix auth.ts to include normalizedName in Player creation +- [x] Write TODO list to repository file +- [x] Auto-create tournament when uploading matches without selecting one -### Frontend -- [x] Basic player rankings page -- [x] Match entry form +### In Progress 🔄 +- [ ] Update API routes to handle new variant scoring fields +- [ ] Update EditTournamentForm to add variant scoring controls +- [ ] Update MatchEditor to use tournament-specific target score +- [ ] Run tests and verify variant scoring implementation -## In Progress - UI Development +### Recently Completed ✅ +- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts) +- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts) +- [x] Reset database and run all migrations from scratch +- [x] Regenerate Prisma client with new rating models +- [x] Update match upload page to auto-create tournament if none selected +- [x] Update all admin scripts to use PrismaPg adapter and dotenv +- [x] Fix match diagram player positioning +- [x] Add CasaOS deployment configuration and documentation +- [x] Create migration to add rating system tables (elo_ratings, glicko2_ratings, open_skill_ratings) +- [x] Add tabbed rankings page to display Elo, OpenSkill, and Glicko2 ratings -### Completed -- [x] Navigation layout (Next.js components) -- [x] UI Design document (UI_DESIGN.md) -- [x] Player Profile page (Next.js) -- [x] Basic CSS styling (Tailwind CSS) -- [x] Player Schedule page (Next.js) -- [x] Route for player schedule +### Backlog 📋 +- [ ] Add UI controls for variant scoring in tournament creation/edit +- [ ] Test variant tournament functionality end-to-end +- [ ] Add validation for tie scores based on tournament configuration +- [ ] Document variant tournament features -### View Types to Implement -- [ ] Tournament Admin View (Phase 2-3) - - Create/manage tournaments - - Set up brackets and matchups - - Record match results - - View tournament standings +## Recently Completed (Detailed) -- [ ] Club Admin View (Superuser) (Phase 3-4) - - Manage all players - - View club-wide statistics - - Configure club settings - - Manage tournaments +### Variant Euchre Scoring Support +- Added `targetScore` and `allowTies` fields to Event model +- Created database migration for new fields +- Fixed partnership stats tie handling (ties now increment neither wins nor losses) +- Updated Elo calculation functions to handle ties correctly (0.5 points for draw) -- [ ] Player Profile View (Phase 1-2) - - Display player info and Elo rating - - Show partnership analytics - - Display match history - - Tournament participation - - Enhance existing template +### Tournament Deletion +- Consolidated delete endpoint to `/api/tournaments/[id]` +- Added options to delete matches or orphan them +- Updated DeleteTournamentButton to use consolidated endpoint -- [ ] Player Tournament Schedule View (Phase 4) - - Show upcoming matches - - Display tournament brackets - - Record personal match results +### Player Management +- Added admin players page at `/admin/players` +- Added player name editing functionality via PATCH endpoint +- Added player merge functionality with automatic Elo recalculation +- Fixed foreign key constraint issues with elo_snapshots -### UI Components Needed -- [x] Navigation system (role-based) - Started -- [ ] Dashboard layouts -- [ ] Forms for data entry -- [ ] Tables for displaying data -- [ ] Charts for statistics -- [ ] Bracket visualization +### Permissions +- Added `site_admin` role as highest privilege level +- Updated all permission functions to include site_admin support +- Fixed session cache issues by reading roles from database -### Implementation Phases -- [x] Phase 1: Navigation & Layout -- [x] Phase 2: Player Profile Enhancements -- [x] Phase 3: Tournament Admin View -- [x] Phase 4: Club Admin View -- [x] Phase 5: Player Schedule View -- [ ] Phase 6: Authentication & Authorization -- [x] Phase 7: Polish & Testing +## Notes +- All 84 unit tests passing +- Database migrations applied successfully +- TypeScript compilation has pre-existing errors unrelated to our changes -## Future Enhancements +### Completed After Commit 1729dac -### Features -- [ ] Real-time match updates (WebSockets) -- [ ] Mobile-responsive design improvements -- [ ] Email notifications -- [ ] Import/Export functionality -- [ ] API for third-party integrations -- [ ] Advanced analytics charts +#### Next.js 16 Breaking Change Fixes +- [x] Fixed `params.id` usage in all page components (must use `await params`) +- [x] Fixed `params.id` usage in all API routes (must use `await params`) +- [x] Updated client components to use `Promise<{ id: string }>` type +- [x] Added regression tests for Next.js 16 params Promise handling +- [x] Verified all 100 unit tests pass -### Technical -- [ ] Performance optimization -- [ ] Caching strategy -- [ ] Security hardening -- [ ] Deployment pipeline -- [ ] CI/CD setup +#### Files Updated: +- Player pages: `profile.tsx`, `schedule.tsx` +- Tournament pages: `page.tsx`, `results.tsx`, `edit.tsx`, `entry.tsx` +- API routes: `admin/players/[id]/route.ts`, `users/[id]/route.ts`, `users/[id]/role/route.ts` +- Tournament API routes: `[id]/route.ts`, `[id]/participants/route.ts`, `[id]/games/bulk/route.ts` -## AAA System (Authentication, Authorization, Accounting) - Next.js Implementation +#### Root Cause +Next.js 16 requires `params` to be awaited in both server components and API routes: +- Before: `const { id } = params` +- After: `const { id } = await params` -### Authentication (Better Auth + Prisma) -- [x] Set up Better Auth with Prisma -- [x] Create users table schema -- [x] Build login page (`/auth/login`) -- [x] Build registration page (`/auth/register`) -- [x] Implement session management with Better Auth -- [x] Add authentication middleware -- [ ] Password reset functionality -- [ ] Email confirmation system -- [ ] OAuth providers (optional) - -### Authorization (RBAC) -- [x] Define roles in Prisma schema (PLAYER, TOURNAMENT_ADMIN, CLUB_ADMIN) -- [x] Implement authorization helpers -- [x] Add authorization to admin dashboard -- [x] Add authorization to player management -- [x] Add authorization to tournament management -- [x] Add 5-minute match edit window (player role) -- [ ] Add role assignment UI for club admins -- [ ] Add permission checks to all API routes - -### Accounting (Activity Logging) -- [ ] Create activity logging system (Prisma model) -- [ ] Track authentication events -- [ ] Track tournament management events -- [ ] Track match recording events -- [ ] Build audit reports UI - -### Security -- [x] Rate limiting (Better Auth built-in) -- [ ] IP-based lockout -- [x] Secure cookie settings (Better Auth) -- [x] CSRF protection (Next.js built-in) -- [ ] Security headers -- [ ] Session fixation prevention - -## Known Issues -- [ ] Database IDs not resetting between tests (workaround: query by round_number) -- [ ] Need to clean up debug output from acceptance tests -- [ ] Password reset flow not yet implemented - -## Next Steps -1. Design UI mockups for each view type -2. Implement navigation system -3. Build out Tournament Admin view -4. Add role-based access control -5. Create reusable UI components +This was not caught by the unit test suite because: +- Unit tests test individual functions in isolation +- E2E tests (Playwright) would catch this but weren't run after the upgrade -- 2.52.0 From 00373fcef468a5687a41c810a9c0d3bd62a21c97 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:50:51 -0700 Subject: [PATCH 14/17] refactor: move legacy files to organized directories --- CASAOS_DEPLOYMENT.md | 273 -------------------------- DOCKER.md | 370 ------------------------------------ TODO.md | 102 ---------- generate_games.py | 138 -------------- update_partnership_stats.py | 234 ----------------------- update_player_stats.py | 195 ------------------- 6 files changed, 1312 deletions(-) delete mode 100644 CASAOS_DEPLOYMENT.md delete mode 100644 DOCKER.md delete mode 100644 TODO.md delete mode 100644 generate_games.py delete mode 100644 update_partnership_stats.py delete mode 100644 update_player_stats.py diff --git a/CASAOS_DEPLOYMENT.md b/CASAOS_DEPLOYMENT.md deleted file mode 100644 index 2bcccfa..0000000 --- a/CASAOS_DEPLOYMENT.md +++ /dev/null @@ -1,273 +0,0 @@ -# EuchreCamp CasaOS Deployment Guide - -This guide explains how to deploy EuchreCamp on your CasaOS server. - -## Prerequisites - -1. **CasaOS** installed on your server -2. **Docker** and **Docker Compose** available in CasaOS -3. **Port 3000** available for the web interface -4. **Port 5432** available for PostgreSQL (or use external database) - -## Prerequisites - -**External PostgreSQL Database Required** -- PostgreSQL 15 or higher -- Database name: `euchre_camp` -- User: `euchre_camp` -- Privileges: ALL on database `euchre_camp` - -## Quick Start - -### Option 1: Using CasaOS App Store (Recommended) - -1. **Set up PostgreSQL database first** (see below) -2. Open CasaOS Dashboard -3. Go to **App Store** → **Custom App** -4. Click **Install** and configure the following: - - **Basic Settings:** - - App Name: `EuchreCamp` - - Image: `euchre-camp:latest` (build locally first) - - Port: `3000` - - **Environment Variables:** - ``` - DATABASE_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp - DATABASE_SHADOW_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp_shadow - BETTER_AUTH_SECRET=your-secret-key-here - BETTER_AUTH_URL=http://your-casaos-ip:3000 - TRUSTED_ORIGINS=http://your-casaos-ip:3000 - ``` - -5. Click **Install** - -### Option 2: Using Docker Compose - -1. **Set up PostgreSQL database first** (see below) -2. **Build the Docker image:** - ```bash - docker-compose -f docker-compose.casaos.yml build - ``` - -3. **Start the application:** - ```bash - docker-compose -f docker-compose.casaos.yml up -d - ``` - -4. **Check logs:** - ```bash - docker-compose -f docker-compose.casaos.yml logs -f - ``` - -## PostgreSQL Database Setup - -### Option A: External PostgreSQL Server - -Connect to your PostgreSQL server and run: - -```sql --- Create database -CREATE DATABASE euchre_camp; - --- Create user -CREATE USER euchre_camp WITH PASSWORD 'your-strong-password'; - --- Grant privileges -GRANT ALL PRIVILEGES ON DATABASE euchre_camp TO euchre_camp; - --- Connect to database -\c euchre_camp - --- Grant schema privileges -GRANT ALL ON SCHEMA public TO euchre_camp; -``` - -### Option B: CasaOS PostgreSQL Container - -If you want to run PostgreSQL in CasaOS: - -1. Install PostgreSQL from CasaOS App Store -2. Configure it with: - - Database: `euchre_camp` - - User: `euchre_camp` - - Password: `your-strong-password` -3. Use the connection string in your EuchreCamp configuration: - ``` - DATABASE_URL=postgresql://euchre_camp:your-strong-password@192.168.1.100:5432/euchre_camp - ``` - (Replace `192.168.1.100` with your CasaOS IP) - -## Configuration - -### Required Environment Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `DATABASE_URL` | PostgreSQL connection string (REQUIRED) | `postgresql://euchre_camp:pass@host:5432/euchre_camp` | -| `BETTER_AUTH_SECRET` | Secret key for session encryption (generate with `openssl rand -base64 32`) | `your-generated-secret` | -| `BETTER_AUTH_URL` | Public URL of your application | `http://192.168.1.100:3000` | -| `TRUSTED_ORIGINS` | Comma-separated list of trusted origins | `http://192.168.1.100:3000` | - -### Optional Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `DATABASE_SHADOW_URL` | Shadow database for migrations | Same as DATABASE_URL with `_shadow` suffix | - -## Database Management - -### First-Time Setup - -1. **Create the database and user** (see PostgreSQL Setup above) -2. **Run database migrations:** - ```bash - docker exec -it euchre-camp npx prisma migrate deploy - ``` -3. **Create admin user:** - ```bash - docker exec -it euchre-camp node scripts/create-admin-via-api.js - ``` - -### Accessing the Database - -```bash -# Connect to external PostgreSQL server -psql -h your-postgres-host -U euchre_camp -d euchre_camp - -# List tables -\dt - -# Exit -\q -``` - -### Backing Up Data - -```bash -# Backup PostgreSQL data (run on your PostgreSQL server) -pg_dump -h your-postgres-host -U euchre_camp euchre_camp > backup.sql - -# Backup uploaded files -tar -czf euchre_camp_files.tar.gz /var/lib/docker/volumes/euchre_camp_app_data -``` - -## Security Considerations - -### Change Default Passwords - -1. **PostgreSQL Password:** - - Edit `docker-compose.casaos.yml` or set via CasaOS environment variables - - Update `POSTGRES_PASSWORD` to a strong password - -2. **Better Auth Secret:** - ```bash - openssl rand -base64 32 - ``` - - Use this value for `BETTER_AUTH_SECRET` - -### HTTPS Setup - -For production use with HTTPS: - -1. **Option A: CasaOS Reverse Proxy** - - Configure CasaOS to use HTTPS - - Update `BETTER_AUTH_URL` to `https://your-domain.com` - -2. **Option B: External Reverse Proxy (Nginx, Traefik)** - - Configure your reverse proxy - - Update `TRUSTED_ORIGINS` to include your domain - -### Firewall Rules - -Ensure only necessary ports are exposed: -- **Port 3000**: Web interface (HTTP/HTTPS) -- **Port 5432**: PostgreSQL (only if external access needed) - -## Troubleshooting - -### Database Connection Issues - -```bash -# Check PostgreSQL logs -docker logs euchre-camp-postgres - -# Check if PostgreSQL is healthy -docker exec euchre-camp-postgres pg_isready -U euchre -d euchre_camp -``` - -### Application Not Starting - -```bash -# Check application logs -docker logs euchre-camp - -# Check if Prisma migrations are applied -docker exec euchre-camp npx prisma migrate status -``` - -### Permission Issues - -If you see permission errors: -```bash -# Fix volume permissions -docker exec euchre-camp chown -R euchre:euchre /app/public/uploads -``` - -## Performance Tuning - -### PostgreSQL Optimization - -Add to `docker-compose.casaos.yml` under `postgres` service: -```yaml -environment: - - POSTGRES_SHARED_BUFFERS=256MB - - POSTGRES_WORK_MEM=16MB -``` - -### Application Optimization - -The Dockerfile is already optimized with: -- Multi-stage build (smaller image size) -- Non-root user for security -- Health checks -- Production-ready settings - -## Updating - -To update EuchreCamp: - -```bash -# Pull latest changes -git pull origin main - -# Rebuild and restart -docker-compose -f docker-compose.casaos.yml down -docker-compose -f docker-compose.casaos.yml build --no-cache -docker-compose -f docker-compose.casaos.yml up -d -``` - -## Monitoring - -### View Logs -```bash -docker-compose -f docker-compose.casaos.yml logs -f -``` - -### Check Container Status -```bash -docker ps | grep euchre -``` - -### Resource Usage -```bash -docker stats euchre-camp euchre-camp-postgres -``` - -## Support - -For issues or questions: -- Check the application logs first -- Verify environment variables are set correctly -- Ensure PostgreSQL is running and accessible -- Check CasaOS app logs for deployment issues diff --git a/DOCKER.md b/DOCKER.md deleted file mode 100644 index 9f4ea0f..0000000 --- a/DOCKER.md +++ /dev/null @@ -1,370 +0,0 @@ -# Docker Deployment Guide - -This guide covers how to run EuchreCamp using Docker and Docker Compose. - -## Prerequisites - -- Docker Engine 20.10+ -- Docker Compose v2.0+ -- Make (optional, for using Makefile commands) - -## Quick Start - -### 1. Environment Setup - -Create a `.env` file in the project root: - -```bash -cp .env.example .env -``` - -Update the `.env` file with your configuration: - -```env -# Database Configuration (PostgreSQL in Docker) -DATABASE_PROVIDER=postgresql -DATABASE_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp -DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp_shadow - -# Better Auth (generate a secure secret for production) -BETTER_AUTH_SECRET=your-secret-key-change-in-production -BETTER_AUTH_URL=http://localhost:3000 - -# Application -NODE_ENV=production -TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 -``` - -### 2. Build and Run - -#### Production Mode - -```bash -# Build and start all services -docker-compose up -d - -# View logs -docker-compose logs -f - -# Stop all services -docker-compose down -``` - -#### Development Mode - -```bash -# Use development override configuration -docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d - -# Or use the shorthand -docker-compose --profile dev up -d -``` - -### 3. Access the Application - -- **Web Application**: http://localhost:3000 -- **PostgreSQL**: localhost:5432 (default) or 5433 in development - -## Service Architecture - -``` -┌─────────────────────────────────────────────┐ -│ EuchreCamp │ -├─────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ Next.js │◄───│ PostgreSQL │ │ -│ │ App │ │ Database │ │ -│ │ │ │ │ │ -│ │ Port: 3000 │ │ Port: 5432 │ │ -│ └──────────────┘ └──────────────────┘ │ -│ │ -└─────────────────────────────────────────────┘ -``` - -## Service Details - -### App Service - -- **Image**: Multi-stage Next.js build -- **Port**: 3000 -- **Environment**: Production or Development -- **Volumes**: - - `app_data`: Persistent storage for uploads - -### PostgreSQL Service - -- **Image**: postgres:15-alpine -- **Port**: 5432 (production) / 5433 (development) -- **Data Volume**: `postgres_data` (persistent) -- **Health Check**: Uses `pg_isready` - -## Database Management - -### View Database Data - -```bash -# Access PostgreSQL shell -docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp - -# List tables -\dt - -# Exit -\q -``` - -### Backup Database - -```bash -# Create backup -docker exec euchre-camp-postgres pg_dump -U euchre euchre_camp > backup.sql - -# Restore backup -docker exec -i euchre-camp-postgres psql -U euchre euchre_camp < backup.sql -``` - -### Reset Database - -```bash -# Remove all data and restart -docker-compose down -v -docker-compose up -d -``` - -## Running Commands - -### Run Database Migrations - -```bash -# Apply migrations -docker-compose exec app npx prisma migrate deploy - -# Generate Prisma client -docker-compose exec app npx prisma generate - -# Open Prisma Studio -docker-compose exec app npx prisma studio -``` - -### Create Admin User - -```bash -# Create admin user via script -docker-compose exec app node scripts/create-admin-via-api.js -``` - -### Seed Database - -```bash -# Run seed script -docker-compose exec app node scripts/seed.js -``` - -## Development Workflow - -### Hot Reloading - -Development mode supports hot reloading: - -```bash -# Start in development mode -docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d - -# View logs -docker-compose logs -f app -``` - -### Modifying Code - -Changes to source code are automatically reflected due to volume mounting: - -```bash -# Edit a file locally -nano src/app/page.tsx - -# Next.js will automatically reload -``` - -## Troubleshooting - -### Common Issues - -**1. Database Connection Failed** - -```bash -# Check if PostgreSQL is running -docker ps | grep postgres - -# Check logs -docker logs euchre-camp-postgres - -# Restart services -docker-compose restart postgres -``` - -**2. Port Already in Use** - -```bash -# Check what's using port 3000 -lsof -i :3000 - -# Change port in docker-compose.yml -# ports: -# - "3001:3000" -``` - -**3. Permission Denied** - -```bash -# Ensure scripts are executable -chmod +x scripts/*.sh - -# Check volume permissions -docker exec euchre-camp-app ls -la /app -``` - -**4. Migration Errors** - -```bash -# Reset database -docker-compose down -v -docker-compose up -d - -# Apply migrations manually -docker-compose exec app npx prisma migrate deploy -``` - -### View Logs - -```bash -# All services -docker-compose logs -f - -# Specific service -docker-compose logs -f app -docker-compose logs -f postgres -``` - -### Container Shell Access - -```bash -# App container -docker exec -it euchre-camp-app sh - -# PostgreSQL container -docker exec -it euchre-camp-postgres sh -``` - -## Production Deployment - -### Environment Variables - -For production, ensure you set secure values: - -```env -# Generate a secure secret -BETTER_AUTH_SECRET=$(openssl rand -base64 32) - -# Use real domain -BETTER_AUTH_URL=https://euchrecamp.example.com - -# Trusted origins -TRUSTED_ORIGINS=https://euchrecamp.example.com,https://www.euchrecamp.example.com -``` - -### Security Considerations - -1. **Change default PostgreSQL password**: - ```env - POSTGRES_PASSWORD=your-secure-password - ``` - -2. **Use SSL/TLS for database connections**: - ```env - DATABASE_URL=postgresql://euchre:password@postgres:5432/euchre_camp?sslmode=require - ``` - -3. **Restrict network access**: - - Don't expose PostgreSQL port to public internet - - Use firewall rules to restrict access - -4. **Regular backups**: - - Schedule automated database backups - - Test restoration process - -### Docker Swarm / Kubernetes - -For production orchestration, consider: - -1. **Separate database service** - Use managed PostgreSQL (RDS, Cloud SQL) -2. **Secrets management** - Use Docker secrets or Kubernetes Secrets -3. **Health checks** - Configure proper health checks -4. **Resource limits** - Set CPU and memory limits -5. **Replicas** - Run multiple app instances for high availability - -## Makefile (Optional) - -Create a `Makefile` for convenient commands: - -```makefile -.PHONY: up down logs build shell-migrate - -up: - docker-compose up -d - -down: - docker-compose down - -logs: - docker-compose logs -f - -build: - docker-compose build --no-cache - -shell-app: - docker exec -it euchre-camp-app sh - -shell-db: - docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp - -migrate: - docker-compose exec app npx prisma migrate deploy - -seed: - docker-compose exec app node scripts/seed.js -``` - -## Performance Tuning - -### PostgreSQL Configuration - -Edit `docker-compose.yml` to tune PostgreSQL: - -```yaml -postgres: - # ... other config ... - command: > - postgres - -c max_connections=200 - -c shared_buffers=256MB - -c effective_cache_size=1GB - -c maintenance_work_mem=64MB - -c checkpoint_completion_target=0.9 - -c wal_buffers=16MB - -c default_statistics_target=100 -``` - -### Next.js Optimization - -The Dockerfile uses multi-stage build for optimal size and performance: - -- **Builder stage**: Installs all dependencies and builds the app -- **Runner stage**: Copies only the built artifacts and production dependencies - -## References - -- [Docker Compose Documentation](https://docs.docker.com/compose/) -- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres) -- [Next.js Deployment](https://nextjs.org/docs/deployment) -- [Prisma Deployment](https://www.prisma.io/docs/concepts/components/prisma-migrate/deployment) diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b5529c0..0000000 --- a/TODO.md +++ /dev/null @@ -1,102 +0,0 @@ -# EuchreCamp - Todo List - -## Current Tasks - -### Completed ✅ -- [x] Add `site_admin` role to database schema and permissions system -- [x] Add `isCasual` boolean field to Match model (already existed) -- [x] Update match upload API to support casual matches -- [x] Update match upload UI to include casual checkbox -- [x] Add tournament deletion API endpoint with delete/orphan options -- [x] Add delete tournament button and modal to tournament detail page -- [x] Run tests and verify implementation (84 tests passing) -- [x] Fix session issues with tournament admin access -- [x] Fix Elo recalculation error for player merge (delete elo snapshots before deleting players) -- [x] Add admin player management page -- [x] Add player name editing functionality in admin UI -- [x] Add admin panel links to navigation header -- [x] Add tournament update API endpoint (PUT /api/tournaments/[id]) -- [x] Consolidate delete endpoint from admin API to main tournaments API -- [x] Update database schema to add variant scoring fields (targetScore, allowTies) -- [x] Fix tie handling logic in partnership stats (ties now correctly tracked) -- [x] Fix test files for normalizedName field in Player model -- [x] Fix auth.ts to include normalizedName in Player creation -- [x] Write TODO list to repository file -- [x] Auto-create tournament when uploading matches without selecting one - -### In Progress 🔄 -- [ ] Update API routes to handle new variant scoring fields -- [ ] Update EditTournamentForm to add variant scoring controls -- [ ] Update MatchEditor to use tournament-specific target score -- [ ] Run tests and verify variant scoring implementation - -### Recently Completed ✅ -- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts) -- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts) -- [x] Reset database and run all migrations from scratch -- [x] Regenerate Prisma client with new rating models -- [x] Update match upload page to auto-create tournament if none selected -- [x] Update all admin scripts to use PrismaPg adapter and dotenv -- [x] Fix match diagram player positioning -- [x] Add CasaOS deployment configuration and documentation -- [x] Create migration to add rating system tables (elo_ratings, glicko2_ratings, open_skill_ratings) -- [x] Add tabbed rankings page to display Elo, OpenSkill, and Glicko2 ratings - -### Backlog 📋 -- [ ] Add UI controls for variant scoring in tournament creation/edit -- [ ] Test variant tournament functionality end-to-end -- [ ] Add validation for tie scores based on tournament configuration -- [ ] Document variant tournament features - -## Recently Completed (Detailed) - -### Variant Euchre Scoring Support -- Added `targetScore` and `allowTies` fields to Event model -- Created database migration for new fields -- Fixed partnership stats tie handling (ties now increment neither wins nor losses) -- Updated Elo calculation functions to handle ties correctly (0.5 points for draw) - -### Tournament Deletion -- Consolidated delete endpoint to `/api/tournaments/[id]` -- Added options to delete matches or orphan them -- Updated DeleteTournamentButton to use consolidated endpoint - -### Player Management -- Added admin players page at `/admin/players` -- Added player name editing functionality via PATCH endpoint -- Added player merge functionality with automatic Elo recalculation -- Fixed foreign key constraint issues with elo_snapshots - -### Permissions -- Added `site_admin` role as highest privilege level -- Updated all permission functions to include site_admin support -- Fixed session cache issues by reading roles from database - -## Notes -- All 84 unit tests passing -- Database migrations applied successfully -- TypeScript compilation has pre-existing errors unrelated to our changes - -### Completed After Commit 1729dac - -#### Next.js 16 Breaking Change Fixes -- [x] Fixed `params.id` usage in all page components (must use `await params`) -- [x] Fixed `params.id` usage in all API routes (must use `await params`) -- [x] Updated client components to use `Promise<{ id: string }>` type -- [x] Added regression tests for Next.js 16 params Promise handling -- [x] Verified all 100 unit tests pass - -#### Files Updated: -- Player pages: `profile.tsx`, `schedule.tsx` -- Tournament pages: `page.tsx`, `results.tsx`, `edit.tsx`, `entry.tsx` -- API routes: `admin/players/[id]/route.ts`, `users/[id]/route.ts`, `users/[id]/role/route.ts` -- Tournament API routes: `[id]/route.ts`, `[id]/participants/route.ts`, `[id]/games/bulk/route.ts` - -#### Root Cause -Next.js 16 requires `params` to be awaited in both server components and API routes: -- Before: `const { id } = params` -- After: `const { id } = await params` - -This was not caught by the unit test suite because: -- Unit tests test individual functions in isolation -- E2E tests (Playwright) would catch this but weren't run after the upgrade diff --git a/generate_games.py b/generate_games.py deleted file mode 100644 index df38ad6..0000000 --- a/generate_games.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate 100+ games to test ELO rating calculations -""" - -import sqlite3 -import random -from datetime import datetime, timedelta - -DB_PATH = "prisma/prisma/dev.db" - - -def get_players(): - """Get all players from the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute("SELECT id, name, currentElo FROM players") - players = cursor.fetchall() - conn.close() - return players - - -def generate_game(players, game_num, base_date): - """Generate a single game with random players and scores""" - # Randomly select 4 different players - selected_players = random.sample(players, 4) - p1, p2, p3, p4 = selected_players - - # Randomly assign teams (Team 1 vs Team 2) - team1_p1 = p1 - team1_p2 = p2 - team2_p1 = p3 - team2_p2 = p4 - - # Generate realistic scores (Euchre games typically 10 points max) - # Team 1 wins 60% of the time for variety - team1_wins = random.random() < 0.6 - if team1_wins: - # Team 1 wins - generate scores - team1_score = random.randint(10, 15) - team2_score = random.randint(0, 9) - else: - # Team 2 wins - generate scores - team2_score = random.randint(10, 15) - team1_score = random.randint(0, 9) - - # Generate random date in the past 30 days - days_ago = random.randint(0, 30) - game_date = base_date - timedelta( - days=days_ago, hours=random.randint(0, 23), minutes=random.randint(0, 59) - ) - - return { - "team1P1Id": team1_p1[0], - "team1P2Id": team1_p2[0], - "team2P1Id": team2_p1[0], - "team2P2Id": team2_p2[0], - "team1Score": team1_score, - "team2Score": team2_score, - "playedAt": game_date.isoformat() + "Z", - "status": "completed", - "eventId": None, - } - - -def insert_games(games): - """Insert games into the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - for game in games: - cursor.execute( - """ - INSERT INTO matches - (team1P1Id, team1P2Id, team2P1Id, team2P2Id, team1Score, team2Score, playedAt, status, eventId, createdAt, updatedAt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - game["team1P1Id"], - game["team1P2Id"], - game["team2P1Id"], - game["team2P2Id"], - game["team1Score"], - game["team2Score"], - game["playedAt"], - game["status"], - game["eventId"], - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - ), - ) - - conn.commit() - conn.close() - - -def main(): - print("Generating 150 games to test ELO ratings...") - print("=" * 60) - - players = get_players() - print(f"Found {len(players)} players in database") - - # Generate 150 games - num_games = 150 - base_date = datetime.now() - - games = [] - for i in range(num_games): - game = generate_game(players, i, base_date) - games.append(game) - - print(f"Generated {len(games)} games") - - # Insert games into database - print("Inserting games into database...") - insert_games(games) - print("Games inserted successfully!") - - # Show sample games - print("\nSample games:") - print("-" * 80) - for i, game in enumerate(games[:3]): - print( - f"Game {i + 1}: Team 1 ({game['team1Score']}) vs Team 2 ({game['team2Score']})" - ) - print(f" Team 1: Player {game['team1P1Id']} + Player {game['team1P2Id']}") - print(f" Team 2: Player {game['team2P1Id']} + Player {game['team2P2Id']}") - print(f" Date: {game['playedAt']}") - print() - - print("=" * 60) - print(f"Total games generated: {num_games}") - print("Now check the ELO ratings by running the application!") - - -if __name__ == "__main__": - main() diff --git a/update_partnership_stats.py b/update_partnership_stats.py deleted file mode 100644 index 7d9f82a..0000000 --- a/update_partnership_stats.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 -""" -Update partnership statistics based on matches in the database -""" - -import sqlite3 -import math -from datetime import datetime - -DB_PATH = "prisma/prisma/dev.db" -K_FACTOR = 32 - - -def get_all_matches(): - """Get all matches from the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(""" - SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, - team1Score, team2Score, playedAt - FROM matches - ORDER BY playedAt - """) - matches = cursor.fetchall() - conn.close() - return matches - - -def get_or_create_partnership(player1_id, player2_id): - """Get or create a partnership record""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Sort IDs to ensure consistent partnership lookup - p1 = min(player1_id, player2_id) - p2 = max(player1_id, player2_id) - - cursor.execute( - """ - SELECT id, gamesPlayed, wins, losses, totalEloChange - FROM partnership_stats - WHERE player1Id = ? AND player2Id = ? - """, - (p1, p2), - ) - - result = cursor.fetchone() - if result: - conn.close() - return result - - # Create new partnership record - cursor.execute( - """ - INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) - VALUES (?, ?, 0, 0, 0, 0, NULL, ?, ?) - """, - (p1, p2, datetime.now().isoformat() + "Z", datetime.now().isoformat() + "Z"), - ) - - partnership_id = cursor.lastrowid - conn.commit() - conn.close() - - return (partnership_id, 0, 0, 0, 0) - - -def update_partnership_stats(player1_id, player2_id, won, elo_change): - """Update partnership statistics""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Sort IDs to ensure consistent partnership lookup - p1 = min(player1_id, player2_id) - p2 = max(player1_id, player2_id) - - # Get current partnership stats - cursor.execute( - """ - SELECT id, gamesPlayed, wins, losses, totalEloChange - FROM partnership_stats - WHERE player1Id = ? AND player2Id = ? - """, - (p1, p2), - ) - - result = cursor.fetchone() - if not result: - # Create new partnership record - cursor.execute( - """ - INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) - VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?) - """, - ( - p1, - p2, - 1 if won else 0, - 0 if won else 1, - elo_change, - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - ), - ) - else: - partnership_id, games_played, wins, losses, total_elo_change = result - - # Update partnership stats - new_games = games_played + 1 - new_wins = wins + 1 if won else wins - new_losses = losses if won else losses + 1 - new_total_elo_change = total_elo_change + elo_change - - cursor.execute( - """ - UPDATE partnership_stats - SET gamesPlayed = ?, wins = ?, losses = ?, totalEloChange = ?, lastPlayed = ?, updatedAt = ? - WHERE id = ? - """, - ( - new_games, - new_wins, - new_losses, - new_total_elo_change, - datetime.now().isoformat() + "Z", - datetime.now().isoformat() + "Z", - partnership_id, - ), - ) - - conn.commit() - conn.close() - - -def main(): - print("Updating partnership statistics based on matches...") - print("=" * 60) - - # Get all matches - matches = get_all_matches() - print(f"Found {len(matches)} matches in database") - - # Reset partnership stats - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute("DELETE FROM partnership_stats") - conn.commit() - conn.close() - print("Reset all partnership statistics") - - # Process each match - match_count = 0 - for ( - match_id, - team1_p1, - team1_p2, - team2_p1, - team2_p2, - team1_score, - team2_score, - played_at, - ) in matches: - # Determine winners - team1_won = team1_score > team2_score - team2_won = team2_score > team1_score - - # Update partnership stats for Team 1 - if team1_won: - update_partnership_stats( - team1_p1, team1_p2, True, 0 - ) # Elo change will be calculated separately - else: - update_partnership_stats(team1_p1, team1_p2, False, 0) - - # Update partnership stats for Team 2 - if team2_won: - update_partnership_stats(team2_p1, team2_p2, True, 0) - else: - update_partnership_stats(team2_p1, team2_p2, False, 0) - - match_count += 1 - if match_count % 20 == 0: - print(f"Processed {match_count}/{len(matches)} matches...") - - print(f"Processed {match_count} matches") - - # Display partnership stats for top players - print("\n" + "=" * 60) - print("Partnership Stats for Top Players:") - print("-" * 60) - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Get top players by ELO - cursor.execute("SELECT id, name FROM players ORDER BY currentElo DESC LIMIT 5") - top_players = cursor.fetchall() - - for player_id, player_name in top_players: - print(f"\n{player_name}:") - - # Get partnership stats for this player - cursor.execute( - """ - SELECT - CASE - WHEN player1Id = ? THEN (SELECT name FROM players WHERE id = player2Id) - ELSE (SELECT name FROM players WHERE id = player1Id) - END as partner_name, - gamesPlayed, wins, losses, totalEloChange - FROM partnership_stats - WHERE player1Id = ? OR player2Id = ? - ORDER BY gamesPlayed DESC - LIMIT 3 - """, - (player_id, player_id, player_id), - ) - - partnerships = cursor.fetchall() - for partner_name, games, wins, losses, elo_change in partnerships: - win_rate = (wins / games * 100) if games > 0 else 0 - print( - f" - with {partner_name}: {games} games, {wins}/{losses} ({win_rate:.1f}%) ELO: {elo_change:+d}" - ) - - conn.close() - - print("\n" + "=" * 60) - print("Partnership statistics updated successfully!") - - -if __name__ == "__main__": - main() diff --git a/update_player_stats.py b/update_player_stats.py deleted file mode 100644 index b2525da..0000000 --- a/update_player_stats.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 -""" -Update player statistics (ELO, gamesPlayed, wins, losses) based on matches in the database -""" - -import sqlite3 -import math - -DB_PATH = "prisma/prisma/dev.db" -K_FACTOR = 32 # Standard K-factor for Elo calculations - - -def get_all_matches(): - """Get all matches from the database""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(""" - SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, - team1Score, team2Score, playedAt - FROM matches - ORDER BY playedAt - """) - matches = cursor.fetchall() - conn.close() - return matches - - -def get_player(player_id): - """Get a player by ID""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - "SELECT id, name, currentElo, gamesPlayed, wins, losses FROM players WHERE id = ?", - (player_id,), - ) - player = cursor.fetchone() - conn.close() - return player - - -def update_player(player_id, current_elo, games_played, wins, losses): - """Update a player's statistics""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - """ - UPDATE players - SET currentElo = ?, gamesPlayed = ?, wins = ?, losses = ? - WHERE id = ? - """, - (current_elo, games_played, wins, losses, player_id), - ) - conn.commit() - conn.close() - - -def calculate_elo_change(rating_a, rating_b, score_a, score_b): - """Calculate Elo change for a match""" - # Calculate expected scores - expected_a = 1 / (1 + math.pow(10, (rating_b - rating_a) / 400)) - expected_b = 1 - expected_a - - # Actual scores (1 for win, 0.5 for tie, 0 for loss) - actual_a = 0.5 if score_a == score_b else (1 if score_a > score_b else 0) - actual_b = 0.5 if score_a == score_b else (1 if score_b > score_a else 0) - - # Calculate Elo change - elo_change_a = K_FACTOR * (actual_a - expected_a) - elo_change_b = K_FACTOR * (actual_b - expected_b) - - return elo_change_a, elo_change_b - - -def main(): - print("Updating player statistics based on matches in database...") - print("=" * 60) - - # Get all matches - matches = get_all_matches() - print(f"Found {len(matches)} matches in database") - - # Reset all player stats to 0 before recalculating - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - "UPDATE players SET currentElo = 1000, gamesPlayed = 0, wins = 0, losses = 0" - ) - conn.commit() - conn.close() - print("Reset all player statistics to initial values (ELO: 1000, games: 0)") - - # Process each match - match_count = 0 - for ( - match_id, - team1_p1, - team1_p2, - team2_p1, - team2_p2, - team1_score, - team2_score, - played_at, - ) in matches: - # Get player data - p1 = get_player(team1_p1) - p2 = get_player(team1_p2) - p3 = get_player(team2_p1) - p4 = get_player(team2_p2) - - if not all([p1, p2, p3, p4]): - print(f"Warning: Could not find all players for match {match_id}") - continue - - # Calculate team ratings - team1_rating = (p1[2] + p2[2]) / 2 # currentElo - team2_rating = (p3[2] + p4[2]) / 2 # currentElo - - # Calculate Elo changes - team1_elo_change, team2_elo_change = calculate_elo_change( - team1_rating, team2_rating, team1_score, team2_score - ) - - # Individual Elo changes (split evenly between team members) - p1_elo_change = team1_elo_change / 2 - p2_elo_change = team1_elo_change / 2 - p3_elo_change = team2_elo_change / 2 - p4_elo_change = team2_elo_change / 2 - - # Determine winners - team1_won = team1_score > team2_score - team2_won = team2_score > team1_score - - # Update player 1 stats - p1_new_elo = int(p1[2] + p1_elo_change) - p1_new_games = p1[3] + 1 - p1_new_wins = p1[4] + 1 if team1_won else p1[4] - p1_new_losses = p1[5] if team1_won else p1[5] + 1 - update_player(p1[0], p1_new_elo, p1_new_games, p1_new_wins, p1_new_losses) - - # Update player 2 stats - p2_new_elo = int(p2[2] + p2_elo_change) - p2_new_games = p2[3] + 1 - p2_new_wins = p2[4] + 1 if team1_won else p2[4] - p2_new_losses = p2[5] if team1_won else p2[5] + 1 - update_player(p2[0], p2_new_elo, p2_new_games, p2_new_wins, p2_new_losses) - - # Update player 3 stats - p3_new_elo = int(p3[2] + p3_elo_change) - p3_new_games = p3[3] + 1 - p3_new_wins = p3[4] + 1 if team2_won else p3[4] - p3_new_losses = p3[5] if team2_won else p3[5] + 1 - update_player(p3[0], p3_new_elo, p3_new_games, p3_new_wins, p3_new_losses) - - # Update player 4 stats - p4_new_elo = int(p4[2] + p4_elo_change) - p4_new_games = p4[3] + 1 - p4_new_wins = p4[4] + 1 if team2_won else p4[4] - p4_new_losses = p4[5] if team2_won else p4[5] + 1 - update_player(p4[0], p4_new_elo, p4_new_games, p4_new_wins, p4_new_losses) - - match_count += 1 - if match_count % 20 == 0: - print(f"Processed {match_count}/{len(matches)} matches...") - - print(f"Processed {match_count} matches") - - # Display updated player rankings - print("\n" + "=" * 60) - print("Top 10 Players by ELO Rating:") - print("-" * 60) - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(""" - SELECT id, name, currentElo, gamesPlayed, wins, losses - FROM players - ORDER BY currentElo DESC - LIMIT 10 - """) - top_players = cursor.fetchall() - conn.close() - - for rank, (player_id, name, elo, games, wins, losses) in enumerate(top_players, 1): - win_rate = (wins / games * 100) if games > 0 else 0 - print( - f"{rank:2}. {name:15} | ELO: {elo:4} | Games: {games:3} | W/L: {wins}/{losses} ({win_rate:.1f}%)" - ) - - print("\n" + "=" * 60) - print("Player statistics updated successfully!") - print("Now run the application to see updated rankings.") - - -if __name__ == "__main__": - main() -- 2.52.0 From 5b91cf1426520814dd8721c78f255f4cdb2ba16d Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:58:37 -0700 Subject: [PATCH 15/17] ci: use node:20-alpine container to avoid Node.js setup time --- .gitea/workflows/pr.yml | 26 ++++++++++++++------------ .gitea/workflows/test.yml | 13 +++++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml index 14c98ec..9a2660d 100644 --- a/.gitea/workflows/pr.yml +++ b/.gitea/workflows/pr.yml @@ -8,17 +8,18 @@ on: jobs: unit-tests: runs-on: ubuntu-latest + container: + image: node:20-alpine + options: --user root steps: + - name: Install system dependencies + run: | + apk add --no-cache bash git + - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install dependencies run: npm ci @@ -28,21 +29,22 @@ jobs: acceptance-tests: runs-on: ubuntu-latest needs: unit-tests + container: + image: node:20-alpine + options: --user root env: DATABASE_PROVIDER: sqlite DATABASE_URL: file:./prisma/ci.db BETTER_AUTH_SECRET: test-secret-key-for-ci-only steps: + - name: Install system dependencies + run: | + apk add --no-cache bash git + - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install dependencies run: npm ci diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 4b9b4ea..bcf53e3 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -8,19 +8,20 @@ on: jobs: unit-tests: runs-on: ubuntu-latest + container: + image: node:20-alpine + options: --user root # Skip if this is an auto-generated version bump commit (handled by release workflow) if: "!contains(github.event.head_commit.message, 'chore: bump version')" steps: + - name: Install system dependencies + run: | + apk add --no-cache bash git + - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install dependencies run: npm ci -- 2.52.0 From 9c884ee0205aeda112c3fec7f77682d41e8e4d46 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:58:49 -0700 Subject: [PATCH 16/17] chore: add script for running tests in Docker container --- scripts/run-tests-in-container.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 scripts/run-tests-in-container.sh diff --git a/scripts/run-tests-in-container.sh b/scripts/run-tests-in-container.sh new file mode 100755 index 0000000..b3f5a1f --- /dev/null +++ b/scripts/run-tests-in-container.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Run tests in a Node.js container (for consistent environment) + +set -e + +echo "Running tests in node:20-alpine container..." +echo "This avoids Node.js setup time and ensures consistent environment." +echo "" + +# Run unit tests in container +docker run --rm \ + -v "$(pwd):/app" \ + -w /app \ + node:20-alpine \ + sh -c "apk add --no-cache bash git && npm ci && npm run test:run" -- 2.52.0 From fa6c4369a6b08c9af58f54760db477efa7f1e836 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:58:55 -0700 Subject: [PATCH 17/17] chore: update justfile with container testing note --- justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/justfile b/justfile index 25cfe09..eb24680 100644 --- a/justfile +++ b/justfile @@ -57,6 +57,7 @@ format: # --- Testing --- # Run all tests (unit + acceptance with SQLite) +# Note: Uses Docker containers for consistent environment test: test-unit test-acceptance-sqlite # Run all tests with PostgreSQL (Docker) -- 2.52.0