{"id":45952981,"url":"https://github.com/bpg-dev/swimstats","last_synced_at":"2026-04-02T15:15:42.065Z","repository":{"id":334840591,"uuid":"1136475268","full_name":"bpg-dev/swimstats","owner":"bpg-dev","description":"A web app for competitive swimmers (or their parents) to track times, view personal bests, and visualize progress.","archived":false,"fork":false,"pushed_at":"2026-03-26T12:39:00.000Z","size":3029,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-27T05:03:48.408Z","etag":null,"topics":["ai-generated","competitive","go","personal-bests","postgresql","react","self-hosted","swim-times","swimming","time-tracker","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bpg-dev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-17T18:55:51.000Z","updated_at":"2026-03-26T12:38:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bpg-dev/swimstats","commit_stats":null,"previous_names":["bpg/swimstats","bpg-dev/swimstats"],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/bpg-dev/swimstats","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpg-dev%2Fswimstats","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpg-dev%2Fswimstats/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpg-dev%2Fswimstats/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpg-dev%2Fswimstats/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bpg-dev","download_url":"https://codeload.github.com/bpg-dev/swimstats/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpg-dev%2Fswimstats/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31308784,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-generated","competitive","go","personal-bests","postgresql","react","self-hosted","swim-times","swimming","time-tracker","typescript"],"created_at":"2026-02-28T13:13:11.055Z","updated_at":"2026-04-02T15:15:42.057Z","avatar_url":"https://github.com/bpg-dev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SwimStats - Swim Progress Tracker\n\n[![CI](https://github.com/bpg/swimstats/actions/workflows/ci.yaml/badge.svg)](https://github.com/bpg/swimstats/actions/workflows/ci.yaml)\n[![Release](https://img.shields.io/github/v/release/bpg/swimstats)](https://github.com/bpg/swimstats/releases)\n[![codecov](https://codecov.io/gh/bpg/swimstats/branch/main/graph/badge.svg)](https://codecov.io/gh/bpg/swimstats)\n[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)\n[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-FE5196?logo=conventionalcommits\u0026logoColor=white)](https://conventionalcommits.org)\n[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)\n[![Built with Claude Code](https://img.shields.io/badge/Built%20with-Claude%20Code-blueviolet?logo=anthropic)](https://claude.ai/claude-code)\n\n[![Go](https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go\u0026logoColor=white)](https://go.dev)\n[![React](https://img.shields.io/badge/React-18-61DAFB?logo=react\u0026logoColor=black)](https://react.dev)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org)\n[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-4169E1?logo=postgresql\u0026logoColor=white)](https://www.postgresql.org)\n[![TailwindCSS](https://img.shields.io/badge/TailwindCSS-4-06B6D4?logo=tailwindcss\u0026logoColor=white)](https://tailwindcss.com)\n\n[![GHCR Backend](https://img.shields.io/badge/ghcr.io-backend-blue?logo=docker)](https://ghcr.io/bpg/swimstats/backend)\n[![GHCR Frontend](https://img.shields.io/badge/ghcr.io-frontend-blue?logo=docker)](https://ghcr.io/bpg/swimstats/frontend)\n\nA web application for competitive swimmers to track their times, view personal bests, and visualize their progress over time.\n\n\u003e **Note:** This is a personal tracking tool. Swim times are entered manually - there is no automatic import from meet results systems, federation databases, or other external sources.\n\n## Features\n\n- 🏆 **Personal Bests** - Track fastest times across all events with achieved standards badges; click to view all times\n- 🏊 **Record Swim Times** - Log race results with event, time, and meet details\n- ⏱️ **All Times** - Browse complete time history by event with PB indicators and ranking\n- 📅 **Meets** - Organize times by competition with inline quick-add during time entry\n- 🎯 **Time Standards** - Manage time standards with JSON import (Swimming Canada, Swim Ontario), filtered by course and gender\n- 📊 **Comparison** - Compare PBs against standards with adjacent age groups and achievement status\n- 🎯 **Standing Dashboard** - Quick overview showing achieved/almost/not-yet qualification counts\n- 📈 **Progress Charts** - Visualize time progression with PB markers and standard reference lines\n- 🔄 **Course Filtering** - Separate 25m (short course) and 50m (long course) data\n- 📱 **Responsive** - Works on desktop and mobile\n\n## Screenshots\n\n\u003cdetails\u003e\n\u003csummary\u003ePersonal Bests - Track your fastest times\u003c/summary\u003e\n\n![Personal Bests](docs/screenshots/personal-bests.png)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eProgress Charts - Visualize improvement over time\u003c/summary\u003e\n\n![Progress Charts](docs/screenshots/progress.png)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAll Times - Browse time history by event\u003c/summary\u003e\n\n![All Times](docs/screenshots/all-times.png)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMeets - Organize times by competition\u003c/summary\u003e\n\n![Meets](docs/screenshots/meets.png)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eCompare - Check times against standards\u003c/summary\u003e\n\n![Compare](docs/screenshots/compare.png)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eStandards - Manage time standards\u003c/summary\u003e\n\n![Standards](docs/screenshots/standards.png)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSettings - Profile and data management\u003c/summary\u003e\n\n![Settings](docs/screenshots/settings.png)\n\u003c/details\u003e\n\n## Tech Stack\n\n**Backend:**\n\n- Go 1.25+\n- Chi router\n- PostgreSQL with SQLC\n- OIDC authentication (Authentik/Keycloak compatible)\n\n**Frontend:**\n\n- React 18 with TypeScript\n- Vite for build tooling\n- TailwindCSS for styling\n- React Query for server state\n- Recharts for data visualization\n\n## Prerequisites\n\n- [Go 1.25+](https://go.dev/dl/)\n- [Node.js 20+](https://nodejs.org/)\n- [Docker](https://www.docker.com/) \u0026 Docker Compose\n\n## Quick Start (Development Mode)\n\n### 1. Start PostgreSQL\n\n```bash\n# Start the database\ndocker-compose up -d postgres\n\n# Wait for it to be healthy\ndocker-compose ps\n```\n\n### 2. Run Database Migrations\n\nMigrations are embedded in the backend binary and can be run without external tools.\n\n```bash\n# Option A: Using Docker Compose (recommended)\ndocker-compose --profile migrate up migrate\n\n# Option B: Using the backend binary directly\ncd backend \u0026\u0026 go run ./cmd/server migrate\n```\n\n### 3. Start the Backend\n\n```bash\ncd backend\n\n# Install dependencies\ngo mod download\n\n# Run the server in development mode\nENV=development go run ./cmd/server\n```\n\nThe backend will start on `http://localhost:8080`.\n\n### 4. Start the Frontend\n\n```bash\ncd frontend\n\n# Install dependencies\nnpm install\n\n# Start the development server\nnpm run dev\n```\n\nThe frontend will start on `http://localhost:5173`.\n\n### 5. Access the Application\n\nOpen [http://localhost:5173](http://localhost:5173) in your browser.\n\n\u003e **Note:** In development mode (no OIDC configured), the app uses mock authentication. Click \"Continue to App\" on the login page to enter as a dev user with full access.\n\n## Environment Variables\n\n### Backend\n\n| Variable                 | Default                                                                   | Description                                 |\n|--------------------------|---------------------------------------------------------------------------|---------------------------------------------|\n| `PORT`                   | `8080`                                                                    | Server port                                 |\n| `ENV`                    | `development`                                                             | Environment (`development` or `production`) |\n| `DATABASE_URL`           | `postgres://swimstats:swimstats@localhost:5432/swimstats?sslmode=disable` | PostgreSQL connection string                |\n| `OIDC_ISSUER`            | -                                                                         | OIDC provider URL (required in production)  |\n| `OIDC_CLIENT_ID`         | -                                                                         | OAuth2 client ID                            |\n| `OIDC_CLIENT_SECRET`     | -                                                                         | OAuth2 client secret                        |\n| `OIDC_REDIRECT_URL`      | `http://localhost:5173/auth/callback`                                     | OAuth2 redirect URL                         |\n| `OIDC_FULL_ACCESS_CLAIM` | `swimstats_admin`                                                         | Claim/group for full access                 |\n\n### Frontend\n\nCreate a `.env` file in the `frontend/` directory:\n\n```bash\n# Only needed for production OIDC authentication\nVITE_OIDC_AUTHORITY=https://auth.example.com/application/o/swimstats/\nVITE_OIDC_CLIENT_ID=your-client-id\nVITE_OIDC_REDIRECT_URI=http://localhost:5173/auth/callback\nVITE_OIDC_POST_LOGOUT_REDIRECT_URI=http://localhost:5173/\n```\n\n\u003e **Tip:** Leave these unset in development to use mock authentication.\n\n## Running Tests\n\n### Backend Tests\n\n```bash\ncd backend\n\n# Create test database\ncreatedb swimstats_test\n\n# Run migrations on test database using the embedded migrate command\nDATABASE_URL=\"postgres://swimstats:swimstats@localhost:5432/swimstats_test?sslmode=disable\" \\\n  go run ./cmd/server migrate\n\n# Run all tests\ngo test ./...\n\n# Run tests with verbose output\ngo test -v ./...\n\n# Run integration tests only\ngo test -v ./tests/integration/...\n```\n\n### Frontend Tests\n\n```bash\ncd frontend\n\n# Run tests\nnpm test\n\n# Run tests in watch mode\nnpm test -- --watch\n\n# Run tests with UI\nnpm run test:ui\n\n# Run tests with coverage\nnpm run test:coverage\n```\n\n## Project Structure\n\n```\nswimstats/\n├── backend/\n│   ├── cmd/server/          # Application entrypoint\n│   ├── internal/\n│   │   ├── api/             # HTTP handlers and routes\n│   │   │   ├── handlers/    # Request handlers\n│   │   │   └── middleware/  # HTTP middleware\n│   │   ├── auth/            # OIDC authentication\n│   │   ├── domain/          # Business logic (services)\n│   │   │   ├── comparison/  # Personal bests service\n│   │   │   ├── meet/        # Meet service\n│   │   │   ├── standard/    # Time standards service\n│   │   │   ├── swimmer/     # Swimmer service\n│   │   │   └── time/        # Time service\n│   │   └── store/           # Data access layer\n│   │       ├── db/          # SQLC generated code\n│   │       ├── postgres/    # Repository implementations\n│   │       └── queries/     # SQL query definitions\n│   ├── migrations/          # Database migrations\n│   └── tests/               # Integration tests\n├── data/                    # Time standards JSON files for import\n├── frontend/\n│   ├── src/\n│   │   ├── components/      # React components\n│   │   ├── hooks/           # Custom React hooks\n│   │   ├── pages/           # Page components\n│   │   ├── services/        # API client services\n│   │   ├── stores/          # Zustand state stores\n│   │   ├── types/           # TypeScript types\n│   │   └── utils/           # Utility functions\n│   └── tests/               # Frontend tests\n├── specs/                   # Project specifications\n└── docker-compose.yaml      # Docker Compose config\n```\n\n## Common Commands\n\n```bash\n# Regenerate SQLC types after modifying SQL queries\ncd backend \u0026\u0026 sqlc generate\n\n# Format Go code\ncd backend \u0026\u0026 go fmt ./...\n\n# Format frontend code\ncd frontend \u0026\u0026 npm run format\n\n# Lint frontend\ncd frontend \u0026\u0026 npm run lint\n\n# Build frontend for production\ncd frontend \u0026\u0026 npm run build\n\n# Reset database\ndocker-compose down -v\ndocker-compose up -d postgres\n# Then run migrations again\n```\n\n## API Documentation\n\nThe API follows RESTful conventions:\n\n| Endpoint | Methods | Description |\n|----------|---------|-------------|\n| `/api/v1/swimmer` | GET, PUT | Get/update swimmer profile |\n| `/api/v1/meets` | GET, POST | List/create meets |\n| `/api/v1/meets/:id` | GET, PUT, DELETE | Get/update/delete meet |\n| `/api/v1/times` | GET, POST | List/create times |\n| `/api/v1/times/batch` | POST | Create multiple times |\n| `/api/v1/times/:id` | GET, PUT, DELETE | Get/update/delete time |\n| `/api/v1/personal-bests` | GET | Get personal bests |\n| `/api/v1/progress/:event` | GET | Get time progression for an event (query: course_type, start_date, end_date) |\n| `/api/v1/standards` | GET, POST | List/create time standards |\n| `/api/v1/standards/import` | POST | Import single standard with times |\n| `/api/v1/standards/import/json` | POST | Bulk import from JSON file |\n| `/api/v1/standards/:id` | GET, PUT, DELETE | Get/update/delete standard |\n| `/api/v1/standards/:id/times` | PUT | Set all times for a standard |\n| `/api/v1/comparisons` | GET | Compare PBs against a standard (query: standard_id, course_type) |\n| `/api/v1/data/export` | GET | Export all data as JSON backup |\n| `/api/v1/data/import` | POST | Import data (with replace mode) |\n| `/api/v1/data/import/preview` | POST | Preview import showing what will be deleted |\n\nAll endpoints require authentication. In development mode, the backend accepts requests with a mock `Authorization: Bearer dev-token` header or no auth at all (thanks to `ENV=development`).\n\nFor complete API documentation, see [specs/001-swim-progress-tracker/contracts/api.yaml](specs/001-swim-progress-tracker/contracts/api.yaml).\n\n## Troubleshooting\n\n### Database connection refused\n\nMake sure PostgreSQL is running:\n```bash\ndocker-compose ps\ndocker-compose logs postgres\n```\n\n### Migration errors\n\nReset the database and try again:\n```bash\ndocker-compose down -v\ndocker-compose up -d postgres\n# Wait a few seconds, then run migrations\n```\n\n### Frontend proxy errors\n\nEnsure the backend is running on port 8080 before starting the frontend.\n\n**\"Invalid header value char\" error**: This can occur when the mock authentication header contains special characters. The codebase now base64 encodes the `X-Mock-User` header to prevent this issue. If you see this error, make sure both frontend and backend have the latest code.\n\n### Hot Module Replacement (HMR) not working\n\nIf changes to files don't trigger hot reload:\n1. File changes made by external tools (like AI assistants) may not trigger macOS FSEvents\n2. **Solution**: Restart the frontend dev server with `Ctrl+C` then `npm run dev`\n\n### OIDC errors in production\n\nVerify all OIDC environment variables are correctly set and the OIDC provider is accessible.\n\n## How This Was Built\n\nThis project was built entirely using [Claude Opus 4.5](https://www.anthropic.com/claude) and [spec-kit](https://github.com/bpg/spec-kit), with a healthy dose of vibe coding along the way. No hand-written code here - just conversations with an AI, iterating on specs, and watching the codebase come together.\n\nIt started as a fun experiment to see what frontier LLM models with agentic tool chains can actually do when given a real-world project. Turns out, quite a lot! From database schema design to React components to CI/CD pipelines - Claude handled it all.\n\nWas it perfect? Nope. Were there moments of \"wait, that's not what I meant\"? Absolutely. But that's part of the adventure. (Fun fact: the original 200+ commit history had to be squashed before open-sourcing because Claude cheerfully committed test data with real names. Lesson learned about PII and AI assistants!)\n\n## License\n\n[AGPL-3.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbpg-dev%2Fswimstats","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbpg-dev%2Fswimstats","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbpg-dev%2Fswimstats/lists"}