mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 22:12:53 +00:00
Condenses Documentation
This commit is contained in:
@@ -1,307 +0,0 @@
|
||||
# CI/CD Pipeline Documentation
|
||||
|
||||
TurboVault uses GitHub Actions for continuous integration and quality assurance.
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
The CI pipeline runs on:
|
||||
- ✅ Every push to `main` or `develop` branches
|
||||
- ✅ Every pull request to `main` or `develop`
|
||||
|
||||
## Jobs
|
||||
|
||||
### 1. Linting & Security 🔍
|
||||
|
||||
**Purpose:** Code style and security checks
|
||||
|
||||
**Steps:**
|
||||
1. **RuboCop** - Ruby style guide enforcement
|
||||
- Uses `rubocop-rails-omakase` (Rails official style guide)
|
||||
- Runs with `--parallel` for speed
|
||||
- **Fails build** if style violations found
|
||||
|
||||
2. **Brakeman** - Security vulnerability scanner
|
||||
- Scans for Rails security issues
|
||||
- `continue-on-error: true` (warnings don't block PRs)
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
task lint # Check style
|
||||
task lint:fix # Auto-fix issues
|
||||
task security # Run Brakeman
|
||||
```
|
||||
|
||||
### 2. Type Checking 🔷
|
||||
|
||||
**Purpose:** Static type checking with Sorbet
|
||||
|
||||
**Steps:**
|
||||
1. Generate RBI files for gems and Rails DSLs
|
||||
2. Run Sorbet type checker
|
||||
3. **Fails build** if type errors found
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
task tapioca:all # Generate RBI files
|
||||
task typecheck # Run type checker
|
||||
```
|
||||
|
||||
**Note:** RBI generation can take 1-2 minutes in CI.
|
||||
|
||||
### 3. Tests 🧪
|
||||
|
||||
**Purpose:** Run test suite
|
||||
|
||||
**Services:**
|
||||
- PostgreSQL 15 database
|
||||
|
||||
**Steps:**
|
||||
1. Set up PostgreSQL service
|
||||
2. Install system dependencies
|
||||
3. Create test database
|
||||
4. Load schema
|
||||
5. Run test suite
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
task test # Run all tests
|
||||
```
|
||||
|
||||
### 4. Docker Build 🐳
|
||||
|
||||
**Purpose:** Verify Docker image builds successfully
|
||||
|
||||
**Steps:**
|
||||
1. Set up Docker Buildx
|
||||
2. Build production Docker image
|
||||
3. Test that image runs
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
docker build -t turbovault:test .
|
||||
docker run --rm turbovault:test bundle exec ruby --version
|
||||
```
|
||||
|
||||
## CI Workflow File
|
||||
|
||||
Location: `.github/workflows/ci.yml`
|
||||
|
||||
## Status Badges
|
||||
|
||||
Status badges in `README.md`:
|
||||
|
||||
```markdown
|
||||

|
||||
[](https://github.com/rubocop/rubocop)
|
||||
[](https://sorbet.org)
|
||||
```
|
||||
|
||||
**Don't forget** to replace `YOUR_USERNAME` with your actual GitHub username!
|
||||
|
||||
## What Fails the Build?
|
||||
|
||||
| Check | Fails Build? | Why |
|
||||
|-------|--------------|-----|
|
||||
| RuboCop style issues | ✅ Yes | Code style should be consistent |
|
||||
| Brakeman security warnings | ❌ No | Some warnings are false positives |
|
||||
| Sorbet type errors | ✅ Yes | Type safety is important |
|
||||
| Test failures | ✅ Yes | Tests must pass |
|
||||
| Docker build failure | ✅ Yes | Must be deployable |
|
||||
|
||||
## Fixing CI Failures
|
||||
|
||||
### RuboCop Failures
|
||||
|
||||
```bash
|
||||
# Auto-fix most issues
|
||||
task lint:fix
|
||||
|
||||
# Check what's left
|
||||
task lint
|
||||
|
||||
# Commit fixes
|
||||
git add .
|
||||
git commit -m "Fix RuboCop style issues"
|
||||
```
|
||||
|
||||
### Sorbet Type Errors
|
||||
|
||||
```bash
|
||||
# Regenerate RBI files (if gems changed)
|
||||
task tapioca:all
|
||||
|
||||
# Check for errors
|
||||
task typecheck
|
||||
|
||||
# Fix type errors in code
|
||||
# See docs/SORBET.md for patterns
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
|
||||
```bash
|
||||
# Run tests locally
|
||||
task test
|
||||
|
||||
# Run specific test
|
||||
rails test test/models/game_test.rb
|
||||
|
||||
# Fix tests and re-run
|
||||
```
|
||||
|
||||
### Docker Build Failures
|
||||
|
||||
```bash
|
||||
# Build locally to debug
|
||||
docker build -t turbovault:test .
|
||||
|
||||
# Check build logs for errors
|
||||
# Usually missing dependencies or incorrect Dockerfile
|
||||
```
|
||||
|
||||
## CI Performance
|
||||
|
||||
Typical run times:
|
||||
- **Linting & Security:** ~30-60 seconds
|
||||
- **Type Checking:** ~2-3 minutes (RBI generation)
|
||||
- **Tests:** ~1-2 minutes
|
||||
- **Docker Build:** ~3-5 minutes
|
||||
|
||||
**Total:** ~7-11 minutes per run
|
||||
|
||||
## Optimizations
|
||||
|
||||
### Caching
|
||||
|
||||
We use `bundler-cache: true` in Ruby setup to cache gems:
|
||||
```yaml
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
```
|
||||
|
||||
This speeds up runs by ~1-2 minutes.
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
Jobs run in parallel, not sequentially:
|
||||
- Linting, Type Checking, Tests, and Docker Build all run simultaneously
|
||||
- Total time = slowest job (usually Docker Build)
|
||||
|
||||
### RuboCop Parallel
|
||||
|
||||
RuboCop uses `--parallel` to check files concurrently:
|
||||
```bash
|
||||
bundle exec rubocop --parallel
|
||||
```
|
||||
|
||||
## Local Development Workflow
|
||||
|
||||
Before pushing:
|
||||
|
||||
```bash
|
||||
# 1. Fix style
|
||||
task lint:fix
|
||||
|
||||
# 2. Check types
|
||||
task typecheck
|
||||
|
||||
# 3. Run tests
|
||||
task test
|
||||
|
||||
# 4. Commit
|
||||
git add .
|
||||
git commit -m "Your changes"
|
||||
git push
|
||||
```
|
||||
|
||||
This ensures CI will pass!
|
||||
|
||||
## Skipping CI
|
||||
|
||||
To skip CI on a commit (e.g., documentation only):
|
||||
|
||||
```bash
|
||||
git commit -m "Update README [skip ci]"
|
||||
```
|
||||
|
||||
## CI for Pull Requests
|
||||
|
||||
When someone submits a PR:
|
||||
1. CI runs automatically
|
||||
2. All jobs must pass before merge
|
||||
3. Status shown in PR page
|
||||
4. Merge button disabled until CI passes
|
||||
|
||||
## Viewing CI Results
|
||||
|
||||
**On GitHub:**
|
||||
1. Go to repository
|
||||
2. Click **Actions** tab
|
||||
3. Click on a workflow run
|
||||
4. View each job's logs
|
||||
|
||||
**In Pull Requests:**
|
||||
- See status checks at bottom of PR
|
||||
- Click "Details" to view logs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "RBI generation failed"
|
||||
|
||||
**Cause:** Tapioca couldn't generate RBI files
|
||||
|
||||
**Fix:** Usually transient - retry workflow. If persistent, check Gemfile.lock is committed.
|
||||
|
||||
### "Database connection failed"
|
||||
|
||||
**Cause:** PostgreSQL service not ready
|
||||
|
||||
**Fix:** Already handled with health checks:
|
||||
```yaml
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
```
|
||||
|
||||
### "Bundler version mismatch"
|
||||
|
||||
**Cause:** Different Bundler versions locally vs CI
|
||||
|
||||
**Fix:** Run `bundle update --bundler` locally
|
||||
|
||||
## Advanced: Matrix Builds (Future)
|
||||
|
||||
To test multiple Ruby versions:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
ruby-version: ['3.2', '3.3', '3.4']
|
||||
```
|
||||
|
||||
Currently we only test Ruby 3.3.
|
||||
|
||||
## Security
|
||||
|
||||
### Secrets
|
||||
|
||||
We use GitHub Secrets for sensitive data:
|
||||
- `RAILS_MASTER_KEY` (optional, for encrypted credentials)
|
||||
- No other secrets needed for tests
|
||||
|
||||
### Dependabot
|
||||
|
||||
Consider enabling Dependabot to auto-update dependencies:
|
||||
- Settings → Security → Dependabot
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Actions Docs](https://docs.github.com/en/actions)
|
||||
- [RuboCop](https://github.com/rubocop/rubocop)
|
||||
- [Sorbet](https://sorbet.org/)
|
||||
- [Brakeman](https://brakemanscanner.org/)
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Check the workflow file: `.github/workflows/ci.yml`
|
||||
@@ -1,238 +0,0 @@
|
||||
# Demo Account for Development 🎮
|
||||
|
||||
## Quick Login
|
||||
|
||||
**Development Demo Account:**
|
||||
```
|
||||
Email: demo@turbovault.com
|
||||
Password: password123
|
||||
```
|
||||
|
||||
Visit: http://localhost:3000/login
|
||||
|
||||
## What's Included
|
||||
|
||||
The demo account is automatically created when you run `task setup` or `task db:seed` in development mode.
|
||||
|
||||
### Sample Data
|
||||
|
||||
**12 Games** across multiple platforms:
|
||||
- Nintendo 64: Ocarina of Time, Super Mario 64, GoldenEye 007
|
||||
- SNES: Super Metroid
|
||||
- Nintendo Switch: Breath of the Wild, Hades, Stardew Valley
|
||||
- PlayStation 5: Elden Ring, Cyberpunk 2077
|
||||
- PlayStation 2: Final Fantasy VII
|
||||
- PC: Hollow Knight, Portal 2
|
||||
|
||||
**4 Collections:**
|
||||
- **Nintendo Games** - All Nintendo platform games (root collection)
|
||||
- **N64 Classics** - Best N64 games (subcollection)
|
||||
- **All-Time Favorites** - Top-rated games
|
||||
- **To Play** - Backlog games
|
||||
|
||||
**Various Data:**
|
||||
- ✅ Different completion statuses (Completed, Playing, Backlog, On Hold)
|
||||
- ✅ Physical games with conditions (CIB, Loose, Sealed)
|
||||
- ✅ Digital games with store info (Steam, PlayStation Store, eShop)
|
||||
- ✅ User ratings (1-5 stars)
|
||||
- ✅ Storage locations (Shelf A, Shelf B)
|
||||
- ✅ Purchase prices ($9.99 - $85.00)
|
||||
- ✅ Notes and descriptions
|
||||
- ✅ Games organized in multiple collections
|
||||
|
||||
## When to Use Demo Account
|
||||
|
||||
### Good For:
|
||||
- ✅ Quick testing during development
|
||||
- ✅ Demonstrating features
|
||||
- ✅ Testing bulk operations (already has games to select)
|
||||
- ✅ Verifying collection organization
|
||||
- ✅ Testing filters and search (has varied data)
|
||||
- ✅ Screenshot/documentation creation
|
||||
|
||||
### Create Your Own Account For:
|
||||
- Testing registration flow
|
||||
- Testing empty state UI
|
||||
- Testing first-time user experience
|
||||
- Building your actual collection
|
||||
|
||||
## How It Works
|
||||
|
||||
### First Time Setup
|
||||
```bash
|
||||
task setup
|
||||
```
|
||||
|
||||
This runs `rails db:seed` which creates:
|
||||
1. Platforms (31 gaming platforms)
|
||||
2. Genres (30 game genres)
|
||||
3. Demo user
|
||||
4. Demo collections
|
||||
5. Sample games
|
||||
|
||||
### Subsequent Runs
|
||||
The seed file is smart:
|
||||
- Won't duplicate the demo user if it exists
|
||||
- Won't duplicate games if demo user has any
|
||||
- Safe to run `rails db:seed` multiple times
|
||||
|
||||
### Reset Demo Data
|
||||
If you want to reset the demo account:
|
||||
|
||||
```bash
|
||||
# Option 1: Reset entire database
|
||||
task db:reset
|
||||
# This drops, creates, migrates, and re-seeds everything
|
||||
|
||||
# Option 2: Delete just the demo user
|
||||
rails console
|
||||
> User.find_by(email: 'demo@turbovault.com')&.destroy
|
||||
> exit
|
||||
rails db:seed
|
||||
```
|
||||
|
||||
## Credentials Shown
|
||||
|
||||
The demo credentials are displayed:
|
||||
|
||||
1. **In the terminal** when you run `task dev`
|
||||
2. **On the login page** (development only - green box)
|
||||
3. **After seeding** - shows confirmation
|
||||
|
||||
## Security Note
|
||||
|
||||
**Development Only:**
|
||||
- The demo account is ONLY created in development mode
|
||||
- Will NOT be created in production
|
||||
- Safe to commit seed file to git
|
||||
|
||||
**Production:**
|
||||
```ruby
|
||||
if Rails.env.development?
|
||||
# Demo user only created here
|
||||
end
|
||||
```
|
||||
|
||||
## API Testing
|
||||
|
||||
The demo user can also be used for API testing:
|
||||
|
||||
```bash
|
||||
# 1. Create an API token via the web UI
|
||||
# Login as demo@turbovault.com → Settings → API Tokens
|
||||
|
||||
# 2. Or create via console
|
||||
rails console
|
||||
> user = User.find_by(email: 'demo@turbovault.com')
|
||||
> token = user.api_tokens.create!(name: "Testing")
|
||||
> puts token.token
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
### View All Demo Data
|
||||
```ruby
|
||||
rails console
|
||||
> demo = User.find_by(email: 'demo@turbovault.com')
|
||||
> puts "Games: #{demo.games.count}"
|
||||
> puts "Collections: #{demo.collections.count}"
|
||||
> demo.games.each { |g| puts "- #{g.title} (#{g.platform.name})" }
|
||||
```
|
||||
|
||||
### Quick Links After Login
|
||||
- Dashboard: http://localhost:3000/dashboard
|
||||
- All Games: http://localhost:3000/games
|
||||
- Collections: http://localhost:3000/collections
|
||||
- Settings: http://localhost:3000/settings
|
||||
|
||||
### Test Bulk Operations
|
||||
1. Login as demo user
|
||||
2. Go to Games list
|
||||
3. Select multiple games (checkboxes appear)
|
||||
4. Click "Bulk Edit" button
|
||||
5. Update collections, status, location, etc.
|
||||
|
||||
### Test Filters
|
||||
The demo data includes games with:
|
||||
- Multiple platforms (N64, SNES, Switch, PS5, PS2, PC)
|
||||
- Multiple genres (Action, Adventure, RPG, Platformer, etc.)
|
||||
- All completion statuses
|
||||
- Both physical and digital formats
|
||||
- Various ratings
|
||||
|
||||
Perfect for testing search and filter functionality!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Demo user not created?
|
||||
```bash
|
||||
# Check environment
|
||||
rails console
|
||||
> Rails.env
|
||||
=> "development" # Should be development
|
||||
|
||||
# Re-run seeds
|
||||
rails db:seed
|
||||
```
|
||||
|
||||
### Demo user exists but no games?
|
||||
```bash
|
||||
rails console
|
||||
> demo = User.find_by(email: 'demo@turbovault.com')
|
||||
> demo.games.destroy_all # Clear existing
|
||||
> exit
|
||||
|
||||
rails db:seed # Re-create sample games
|
||||
```
|
||||
|
||||
### Can't login?
|
||||
- Email: `demo@turbovault.com` (exact spelling)
|
||||
- Password: `password123` (all lowercase)
|
||||
- Make sure database is migrated: `rails db:migrate`
|
||||
|
||||
### Want fresh demo data?
|
||||
```bash
|
||||
task db:reset
|
||||
# Drops DB, recreates, migrates, seeds everything fresh
|
||||
```
|
||||
|
||||
## Sample Collection Structure
|
||||
|
||||
```
|
||||
Nintendo Games (5 games)
|
||||
├─ N64 Classics (3 games)
|
||||
│ ├─ The Legend of Zelda: Ocarina of Time
|
||||
│ ├─ Super Mario 64
|
||||
│ └─ GoldenEye 007
|
||||
├─ Breath of the Wild
|
||||
├─ Hades
|
||||
├─ Stardew Valley
|
||||
└─ Super Metroid
|
||||
|
||||
All-Time Favorites (9 games)
|
||||
├─ Ocarina of Time
|
||||
├─ Super Mario 64
|
||||
├─ Elden Ring
|
||||
├─ Breath of the Wild
|
||||
├─ Super Metroid
|
||||
├─ Hollow Knight
|
||||
├─ Portal 2
|
||||
├─ Hades
|
||||
└─ Stardew Valley
|
||||
|
||||
To Play (2 games)
|
||||
├─ Final Fantasy VII
|
||||
└─ Cyberpunk 2077
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The demo account gives you a fully populated TurboVault instance instantly:
|
||||
- ✅ 12 diverse games
|
||||
- ✅ 4 collections with relationships
|
||||
- ✅ Realistic data (prices, locations, notes)
|
||||
- ✅ All completion statuses represented
|
||||
- ✅ Both physical and digital games
|
||||
- ✅ Perfect for testing and demos
|
||||
|
||||
Just run `task dev` and login! 🚀
|
||||
@@ -1,111 +1,68 @@
|
||||
# TurboVault Deployment Guide
|
||||
# Deployment Guide
|
||||
|
||||
Complete guide for deploying TurboVault to production.
|
||||
Deploy TurboVault to Kubernetes.
|
||||
|
||||
## Table of Contents
|
||||
## Prerequisites
|
||||
|
||||
1. [GitHub Setup](#github-setup)
|
||||
2. [Kubernetes Deployment](#kubernetes-deployment)
|
||||
3. [Database Setup](#database-setup)
|
||||
4. [DNS & SSL](#dns--ssl)
|
||||
5. [Monitoring](#monitoring)
|
||||
- Kubernetes cluster
|
||||
- kubectl configured
|
||||
- PostgreSQL database (already set up)
|
||||
|
||||
---
|
||||
|
||||
## GitHub Setup
|
||||
|
||||
### Push to GitHub
|
||||
## Quick Deploy
|
||||
|
||||
```bash
|
||||
# Run the automated setup script
|
||||
./scripts/setup-github.sh
|
||||
# 1. Build image (auto-builds via GitHub Actions)
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
|
||||
# Or manually:
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/YOUR_USERNAME/turbovault.git
|
||||
git push -u origin main
|
||||
```
|
||||
# 2. Configure secrets
|
||||
cp k8s/secrets.yaml.example k8s/secrets.yaml
|
||||
nano k8s/secrets.yaml
|
||||
|
||||
### Set Up GitHub Actions (Optional)
|
||||
|
||||
Create `.github/workflows/ci.yml` for automated testing and building:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
- run: bundle exec rails db:test:prepare
|
||||
- run: bundle exec rails test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- k3s/k8s cluster running
|
||||
- `kubectl` configured
|
||||
- Docker installed
|
||||
- PostgreSQL database (in-cluster or external)
|
||||
|
||||
### Quick Deploy
|
||||
|
||||
```bash
|
||||
# Automated deployment
|
||||
# 3. Deploy
|
||||
./scripts/deploy-k8s.sh
|
||||
```
|
||||
|
||||
### Manual Deployment
|
||||
## Configuration
|
||||
|
||||
### 1. Update Image
|
||||
|
||||
Edit `k8s/deployment.yaml` and `k8s/migrate-job.yaml`:
|
||||
|
||||
```yaml
|
||||
image: ghcr.io/ryankazokas/turbovault-app:v1.0.0
|
||||
```
|
||||
|
||||
### 2. Database Connection
|
||||
|
||||
Edit `k8s/configmap.yaml`:
|
||||
|
||||
```yaml
|
||||
DATABASE_HOST: "your-postgres-host"
|
||||
DATABASE_NAME: "turbovault_production"
|
||||
DATABASE_USERNAME: "turbovault"
|
||||
```
|
||||
|
||||
### 3. Secrets
|
||||
|
||||
Edit `k8s/secrets.yaml`:
|
||||
|
||||
```bash
|
||||
# 1. Login to Gitea registry
|
||||
docker login gitea.example.com
|
||||
|
||||
# 2. Build and push Docker image
|
||||
docker build -t gitea.example.com/username/turbovault:latest .
|
||||
docker push gitea.example.com/username/turbovault:latest
|
||||
|
||||
# 3. Create Gitea registry secret in k8s
|
||||
kubectl create secret docker-registry gitea-registry \
|
||||
--docker-server=gitea.example.com \
|
||||
--docker-username=your-username \
|
||||
--docker-password=your-gitea-token \
|
||||
--docker-email=your-email@example.com \
|
||||
--namespace=turbovault
|
||||
|
||||
# 2. Create secrets
|
||||
cp k8s/secrets.yaml.example k8s/secrets.yaml
|
||||
# Edit k8s/secrets.yaml with your values
|
||||
|
||||
# 3. Generate Rails secret
|
||||
# Generate Rails secret
|
||||
rails secret
|
||||
# Copy output to k8s/secrets.yaml SECRET_KEY_BASE
|
||||
|
||||
# 4. Deploy to k8s
|
||||
# Add to secrets.yaml:
|
||||
SECRET_KEY_BASE: "output_from_rails_secret"
|
||||
DATABASE_PASSWORD: "your_postgres_password"
|
||||
|
||||
# Optional (for IGDB):
|
||||
IGDB_CLIENT_ID: "your_igdb_client_id"
|
||||
IGDB_CLIENT_SECRET: "your_igdb_client_secret"
|
||||
```
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
kubectl apply -f k8s/configmap.yaml
|
||||
kubectl apply -f k8s/secrets.yaml
|
||||
@@ -116,117 +73,27 @@ kubectl apply -f k8s/service.yaml
|
||||
kubectl apply -f k8s/ingress.yaml
|
||||
```
|
||||
|
||||
### Update Image Reference
|
||||
|
||||
Edit `k8s/deployment.yaml` and `k8s/migrate-job.yaml` with your registry path:
|
||||
|
||||
```yaml
|
||||
# For public registries (GitHub Container Registry, Docker Hub public)
|
||||
# No imagePullSecrets needed!
|
||||
image: ghcr.io/your-username/turbovault:latest
|
||||
|
||||
# For private registries, add imagePullSecrets:
|
||||
imagePullSecrets:
|
||||
- name: registry-secret
|
||||
|
||||
image: your-registry.com/turbovault:latest
|
||||
```
|
||||
|
||||
**Default:** Use GitHub Container Registry (ghcr.io) - free and built-in.
|
||||
**See:** [k8s/README.md](../k8s/README.md) for registry setup details.
|
||||
|
||||
---
|
||||
|
||||
## Database Setup
|
||||
|
||||
### Option 1: External PostgreSQL
|
||||
|
||||
Use an external PostgreSQL instance (recommended for production):
|
||||
|
||||
1. Create database and user:
|
||||
```sql
|
||||
CREATE DATABASE turbovault_production;
|
||||
CREATE USER turbovault WITH PASSWORD 'your-secure-password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE turbovault_production TO turbovault;
|
||||
```
|
||||
|
||||
2. Update `k8s/configmap.yaml`:
|
||||
```yaml
|
||||
DATABASE_HOST: "your-postgres-host.example.com"
|
||||
DATABASE_PORT: "5432"
|
||||
DATABASE_NAME: "turbovault_production"
|
||||
DATABASE_USERNAME: "turbovault"
|
||||
```
|
||||
|
||||
3. Update `k8s/secrets.yaml`:
|
||||
```yaml
|
||||
DATABASE_PASSWORD: "your-secure-password"
|
||||
```
|
||||
|
||||
### Option 2: In-Cluster PostgreSQL
|
||||
|
||||
Deploy PostgreSQL in your cluster using Helm:
|
||||
## Update Deployment
|
||||
|
||||
```bash
|
||||
# Add Bitnami repo
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
helm repo update
|
||||
# Build new version
|
||||
git tag v1.1.0
|
||||
git push origin v1.1.0
|
||||
|
||||
# Install PostgreSQL
|
||||
helm install postgres bitnami/postgresql \
|
||||
--namespace turbovault \
|
||||
--set auth.database=turbovault_production \
|
||||
--set auth.username=turbovault \
|
||||
--set auth.password=changeme \
|
||||
--set primary.persistence.size=10Gi
|
||||
# Update deployment
|
||||
kubectl set image deployment/turbovault \
|
||||
turbovault=ghcr.io/ryankazokas/turbovault-app:v1.1.0 \
|
||||
-n turbovault
|
||||
|
||||
# Connection details
|
||||
DATABASE_HOST: postgres-postgresql
|
||||
DATABASE_PORT: 5432
|
||||
# Watch rollout
|
||||
kubectl rollout status deployment/turbovault -n turbovault
|
||||
```
|
||||
|
||||
---
|
||||
## SSL/TLS
|
||||
|
||||
## DNS & SSL
|
||||
Update `k8s/ingress.yaml` with your domain and TLS configuration.
|
||||
|
||||
### Configure DNS
|
||||
|
||||
Point your domain to your cluster's ingress:
|
||||
|
||||
```bash
|
||||
# Get ingress IP
|
||||
kubectl get ingress -n turbovault
|
||||
|
||||
# Add A record
|
||||
turbovault.example.com -> YOUR_INGRESS_IP
|
||||
```
|
||||
|
||||
### Enable SSL with cert-manager
|
||||
|
||||
```bash
|
||||
# Install cert-manager
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
|
||||
|
||||
# Create ClusterIssuer
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: your-email@example.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: nginx
|
||||
EOF
|
||||
```
|
||||
|
||||
Update `k8s/ingress.yaml`:
|
||||
For Let's Encrypt with cert-manager:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
@@ -235,68 +102,17 @@ metadata:
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- turbovault.example.com
|
||||
- turbovault.yourdomain.com
|
||||
secretName: turbovault-tls
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check pod health
|
||||
kubectl get pods -n turbovault
|
||||
|
||||
# Check application health
|
||||
kubectl port-forward svc/turbovault-service 3000:80 -n turbovault
|
||||
# Visit http://localhost:3000/up
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All pods
|
||||
kubectl logs -f -l app=turbovault -n turbovault
|
||||
|
||||
# Specific pod
|
||||
kubectl logs -f turbovault-xxxxx-xxxxx -n turbovault
|
||||
|
||||
# Previous logs (if crashed)
|
||||
kubectl logs --previous turbovault-xxxxx-xxxxx -n turbovault
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Pods not starting:**
|
||||
```bash
|
||||
kubectl describe pod -l app=turbovault -n turbovault
|
||||
```
|
||||
|
||||
**Database connection failed:**
|
||||
```bash
|
||||
# Test connection
|
||||
kubectl run -it --rm debug --image=postgres:15 --restart=Never -n turbovault -- \
|
||||
psql -h postgres-postgresql -U turbovault -d turbovault_production
|
||||
```
|
||||
|
||||
**Migration failed:**
|
||||
```bash
|
||||
kubectl logs job/turbovault-migrate -n turbovault
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scaling
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
```bash
|
||||
# Scale to 3 replicas
|
||||
# Scale replicas
|
||||
kubectl scale deployment turbovault --replicas=3 -n turbovault
|
||||
|
||||
# Auto-scaling (HPA)
|
||||
# Auto-scaling
|
||||
kubectl autoscale deployment turbovault \
|
||||
--cpu-percent=70 \
|
||||
--min=2 \
|
||||
@@ -304,73 +120,33 @@ kubectl autoscale deployment turbovault \
|
||||
-n turbovault
|
||||
```
|
||||
|
||||
### Vertical Scaling
|
||||
|
||||
Edit `k8s/deployment.yaml`:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
### Database Backup
|
||||
## Monitoring
|
||||
|
||||
```bash
|
||||
# Automated backup (cronjob)
|
||||
kubectl create cronjob pg-backup \
|
||||
--image=postgres:15 \
|
||||
--schedule="0 2 * * *" \
|
||||
--restart=Never \
|
||||
-n turbovault \
|
||||
-- /bin/sh -c "pg_dump -h postgres-postgresql -U turbovault turbovault_production | gzip > /backup/turbovault-$(date +%Y%m%d).sql.gz"
|
||||
# Check status
|
||||
kubectl get pods -n turbovault
|
||||
|
||||
# View logs
|
||||
kubectl logs -f -l app=turbovault -n turbovault
|
||||
|
||||
# Check resources
|
||||
kubectl top pods -n turbovault
|
||||
```
|
||||
|
||||
### Full Backup
|
||||
## Troubleshooting
|
||||
|
||||
### View pod details
|
||||
```bash
|
||||
# Backup all k8s resources
|
||||
kubectl get all -n turbovault -o yaml > turbovault-backup.yaml
|
||||
|
||||
# Backup secrets (encrypted)
|
||||
kubectl get secrets -n turbovault -o yaml > secrets-backup.yaml
|
||||
kubectl describe pod -l app=turbovault -n turbovault
|
||||
```
|
||||
|
||||
---
|
||||
### Test database connection
|
||||
```bash
|
||||
kubectl exec -it deployment/turbovault -n turbovault -- \
|
||||
rails runner "puts ActiveRecord::Base.connection.execute('SELECT 1').first"
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. ✅ Use Kubernetes Secrets (or Sealed Secrets)
|
||||
2. ✅ Enable HTTPS/TLS
|
||||
3. ✅ Set resource limits
|
||||
4. ✅ Use non-root container user
|
||||
5. ✅ Enable Network Policies
|
||||
6. ✅ Regular security updates
|
||||
7. ✅ Database backups
|
||||
8. ✅ Monitor logs
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Kubernetes Documentation](https://kubernetes.io/docs/)
|
||||
- [k3s Documentation](https://docs.k3s.io/)
|
||||
- [Rails Deployment Guide](https://guides.rubyonrails.org/configuring.html)
|
||||
- [TurboVault API Docs](API_DOCUMENTATION.md)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Need help?
|
||||
- 📖 [Full Documentation](../README.md)
|
||||
- 🐛 [Report Issues](https://github.com/yourusername/turbovault/issues)
|
||||
- 💬 [Discussions](https://github.com/yourusername/turbovault/discussions)
|
||||
### View environment
|
||||
```bash
|
||||
kubectl exec -it deployment/turbovault -n turbovault -- env | grep DATABASE
|
||||
```
|
||||
|
||||
@@ -1,684 +1,124 @@
|
||||
# TurboVault - Development Guide
|
||||
# Development Guide
|
||||
|
||||
## Quick Start
|
||||
## Prerequisites
|
||||
|
||||
- Ruby 3.3+
|
||||
- Docker
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Start PostgreSQL
|
||||
# Clone repository
|
||||
git clone https://github.com/ryankazokas/turbovault-app.git
|
||||
cd turbovault
|
||||
|
||||
# Start services (PostgreSQL + Mailpit)
|
||||
task docker:up
|
||||
|
||||
# Setup database (first time only)
|
||||
# Setup database
|
||||
task db:setup
|
||||
|
||||
# Start Rails server
|
||||
task server
|
||||
|
||||
# Or use bin/dev for Tailwind watch mode
|
||||
bin/dev
|
||||
# Run development server
|
||||
task dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000 and create an account!
|
||||
Visit http://localhost:3000
|
||||
|
||||
## Development Workflow
|
||||
**Demo:** `demo@turbovault.com` / `password123`
|
||||
|
||||
### Daily Development
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
task docker:up # PostgreSQL
|
||||
bin/dev # Rails server + Tailwind watcher
|
||||
# Development
|
||||
task dev # Start Rails + CSS watcher
|
||||
task server # Rails only
|
||||
task console # Rails console
|
||||
|
||||
# Database
|
||||
task db:migrate # Run migrations
|
||||
task db:rollback # Rollback migration
|
||||
task db:reset # Reset database
|
||||
task db:seed # Load seed data
|
||||
|
||||
# Testing
|
||||
task test # Run tests
|
||||
task test:system # System tests
|
||||
|
||||
# Code Quality
|
||||
task lint # Check style
|
||||
task lint:fix # Auto-fix style
|
||||
task typecheck # Type checker
|
||||
task security # Security scan
|
||||
|
||||
# Type Checking
|
||||
task tapioca:init # First time only
|
||||
task tapioca:all # After bundle install
|
||||
task typecheck:watch # Watch mode
|
||||
|
||||
# Services
|
||||
task docker:up # Start services
|
||||
task docker:down # Stop services
|
||||
task docker:logs # View logs
|
||||
|
||||
# IGDB
|
||||
task igdb:sync # Manual sync
|
||||
task igdb:status # Check status
|
||||
task igdb:clear # Clear stuck jobs
|
||||
|
||||
# Cleanup
|
||||
task clean # Remove tmp/logs
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
|
||||
```bash
|
||||
# Create a new migration
|
||||
rails generate migration AddFieldToModel field:type
|
||||
|
||||
# Run migrations
|
||||
task db:migrate
|
||||
|
||||
# Rollback last migration
|
||||
task db:rollback
|
||||
|
||||
# Reset database (drops, creates, migrates, seeds)
|
||||
task db:reset
|
||||
|
||||
# Open Rails console
|
||||
task console
|
||||
|
||||
# Check pending migrations
|
||||
rails db:migrate:status
|
||||
```
|
||||
|
||||
### Generating Code
|
||||
|
||||
```bash
|
||||
# Generate a model
|
||||
rails generate model ModelName field:type
|
||||
|
||||
# Generate a controller
|
||||
rails generate controller ControllerName action1 action2
|
||||
|
||||
# Generate a scaffold (model + controller + views)
|
||||
rails generate scaffold ModelName field:type
|
||||
|
||||
# Destroy generated code
|
||||
rails destroy model ModelName
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
task test
|
||||
|
||||
# Run specific test file
|
||||
rails test test/models/game_test.rb
|
||||
|
||||
# Run specific test
|
||||
rails test test/models/game_test.rb:10
|
||||
|
||||
# Run system tests
|
||||
task test:system
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Run RuboCop linter
|
||||
task lint
|
||||
|
||||
# Auto-fix RuboCop issues
|
||||
task lint:fix
|
||||
|
||||
# Security checks
|
||||
task security
|
||||
```
|
||||
|
||||
### Type Checking with Sorbet
|
||||
|
||||
TurboVault uses Sorbet for gradual static type checking.
|
||||
|
||||
```bash
|
||||
# First time setup (after bundle install)
|
||||
task tapioca:init
|
||||
task tapioca:all
|
||||
|
||||
# Run type checker
|
||||
task typecheck
|
||||
|
||||
# Watch mode (re-checks on file changes)
|
||||
task typecheck:watch
|
||||
|
||||
# Update type definitions after gem/model changes
|
||||
task tapioca:gems # After bundle install
|
||||
task tapioca:dsl # After model changes
|
||||
```
|
||||
|
||||
**See [SORBET.md](SORBET.md) for complete type checking guide.**
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── controllers/
|
||||
│ ├── concerns/ # Shared controller modules
|
||||
│ ├── api/v1/ # API controllers
|
||||
│ └── *.rb # Web controllers
|
||||
├── models/
|
||||
│ ├── concerns/ # Shared model modules
|
||||
│ └── *.rb # ActiveRecord models
|
||||
├── views/
|
||||
│ ├── layouts/ # Application layouts
|
||||
│ └── */ # Controller-specific views
|
||||
├── helpers/ # View helpers
|
||||
├── mailers/ # Email mailers
|
||||
└── jobs/ # Background jobs
|
||||
├── controllers/ # Controllers
|
||||
├── models/ # Models
|
||||
├── views/ # ERB templates
|
||||
├── javascript/ # Stimulus controllers
|
||||
├── services/ # Service objects (IgdbService)
|
||||
└── jobs/ # Background jobs (IgdbSyncJob)
|
||||
|
||||
config/
|
||||
├── routes.rb # URL routing
|
||||
├── database.yml # Database configuration
|
||||
└── environments/ # Environment-specific config
|
||||
├── routes.rb # Routes
|
||||
├── database.yml # Database config
|
||||
├── queue.yml # Solid Queue config
|
||||
└── recurring.yml # Recurring jobs (IGDB sync)
|
||||
|
||||
db/
|
||||
├── migrate/ # Database migrations
|
||||
├── seeds.rb # Seed data
|
||||
└── schema.rb # Current database schema
|
||||
|
||||
test/
|
||||
├── models/ # Model tests
|
||||
├── controllers/ # Controller tests
|
||||
├── system/ # End-to-end tests
|
||||
└── fixtures/ # Test data
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Model
|
||||
|
||||
1. Generate the model:
|
||||
```bash
|
||||
rails generate model Post user:references title:string body:text published:boolean
|
||||
```
|
||||
|
||||
2. Edit the migration (add indexes, constraints, RLS if user-scoped):
|
||||
```ruby
|
||||
class CreatePosts < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :posts do |t|
|
||||
t.references :user, null: false, foreign_key: true, index: true
|
||||
t.string :title, null: false
|
||||
t.text :body
|
||||
t.boolean :published, default: false, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :posts, :title
|
||||
|
||||
# Enable RLS if user-scoped
|
||||
execute <<-SQL
|
||||
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY posts_isolation_policy ON posts
|
||||
USING (user_id = current_setting('app.current_user_id', true)::bigint);
|
||||
SQL
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
3. Run the migration:
|
||||
```bash
|
||||
rails db:migrate
|
||||
```
|
||||
|
||||
4. Add associations and validations to the model:
|
||||
```ruby
|
||||
class Post < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :title, presence: true
|
||||
validates :body, presence: true
|
||||
|
||||
scope :published, -> { where(published: true) }
|
||||
end
|
||||
```
|
||||
|
||||
5. Add association to User model:
|
||||
```ruby
|
||||
class User < ApplicationRecord
|
||||
has_many :posts, dependent: :destroy
|
||||
end
|
||||
```
|
||||
|
||||
### Adding a New Controller
|
||||
|
||||
1. Generate the controller:
|
||||
```bash
|
||||
rails generate controller Posts index show new create edit update destroy
|
||||
```
|
||||
|
||||
2. Implement controller actions:
|
||||
```ruby
|
||||
class PostsController < ApplicationController
|
||||
before_action :require_authentication
|
||||
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@posts = current_user.posts.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@post = current_user.posts.build
|
||||
end
|
||||
|
||||
def create
|
||||
@post = current_user.posts.build(post_params)
|
||||
|
||||
if @post.save
|
||||
redirect_to @post, notice: "Post created successfully."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @post.update(post_params)
|
||||
redirect_to @post, notice: "Post updated successfully."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@post.destroy
|
||||
redirect_to posts_path, notice: "Post deleted successfully."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_post
|
||||
@post = current_user.posts.find(params[:id])
|
||||
end
|
||||
|
||||
def post_params
|
||||
params.require(:post).permit(:title, :body, :published)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
3. Add routes:
|
||||
```ruby
|
||||
resources :posts
|
||||
```
|
||||
|
||||
4. Create views in `app/views/posts/`
|
||||
|
||||
### Adding an API Endpoint
|
||||
|
||||
1. Create API controller:
|
||||
```bash
|
||||
mkdir -p app/controllers/api/v1
|
||||
```
|
||||
|
||||
2. Create controller file:
|
||||
```ruby
|
||||
# app/controllers/api/v1/posts_controller.rb
|
||||
module Api
|
||||
module V1
|
||||
class PostsController < BaseController
|
||||
def index
|
||||
@posts = current_user.posts.order(created_at: :desc)
|
||||
render json: @posts
|
||||
end
|
||||
|
||||
def show
|
||||
@post = current_user.posts.find(params[:id])
|
||||
render json: @post
|
||||
end
|
||||
|
||||
def create
|
||||
@post = current_user.posts.build(post_params)
|
||||
|
||||
if @post.save
|
||||
render json: @post, status: :created
|
||||
else
|
||||
render json: { errors: @post.errors.full_messages },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def post_params
|
||||
params.require(:post).permit(:title, :body, :published)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
3. Add API routes:
|
||||
```ruby
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
resources :posts, only: [:index, :show, :create]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Adding a Background Job
|
||||
|
||||
1. Generate the job:
|
||||
```bash
|
||||
rails generate job ProcessData
|
||||
```
|
||||
|
||||
2. Implement the job:
|
||||
```ruby
|
||||
class ProcessDataJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user_id)
|
||||
user = User.find(user_id)
|
||||
# Do some processing
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
3. Enqueue the job:
|
||||
```ruby
|
||||
ProcessDataJob.perform_later(user.id)
|
||||
```
|
||||
|
||||
### Adding Email Functionality
|
||||
|
||||
1. Generate a mailer:
|
||||
```bash
|
||||
rails generate mailer UserMailer welcome
|
||||
```
|
||||
|
||||
2. Implement the mailer:
|
||||
```ruby
|
||||
class UserMailer < ApplicationMailer
|
||||
def welcome(user)
|
||||
@user = user
|
||||
mail(to: @user.email, subject: "Welcome to TurboVault!")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
3. Create email templates in `app/views/user_mailer/`
|
||||
|
||||
4. Configure SMTP in `config/environments/`:
|
||||
```ruby
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
address: ENV['SMTP_ADDRESS'],
|
||||
port: ENV['SMTP_PORT'],
|
||||
user_name: ENV['SMTP_USERNAME'],
|
||||
password: ENV['SMTP_PASSWORD'],
|
||||
authentication: 'plain',
|
||||
enable_starttls_auto: true
|
||||
}
|
||||
```
|
||||
|
||||
5. Send the email:
|
||||
```ruby
|
||||
UserMailer.welcome(user).deliver_later
|
||||
```
|
||||
|
||||
## Testing Guide
|
||||
|
||||
### Writing Model Tests
|
||||
|
||||
```ruby
|
||||
# test/models/game_test.rb
|
||||
require "test_helper"
|
||||
|
||||
class GameTest < ActiveSupport::TestCase
|
||||
test "should not save game without title" do
|
||||
game = Game.new
|
||||
assert_not game.save, "Saved game without title"
|
||||
end
|
||||
|
||||
test "should save valid game" do
|
||||
game = games(:one) # Uses fixture
|
||||
assert game.save, "Failed to save valid game"
|
||||
end
|
||||
|
||||
test "should belong to user" do
|
||||
game = games(:one)
|
||||
assert_respond_to game, :user
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Writing Controller Tests
|
||||
|
||||
```ruby
|
||||
# test/controllers/games_controller_test.rb
|
||||
require "test_helper"
|
||||
|
||||
class GamesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:one)
|
||||
sign_in_as @user # Helper method to sign in
|
||||
@game = games(:one)
|
||||
end
|
||||
|
||||
test "should get index" do
|
||||
get games_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create game" do
|
||||
assert_difference('Game.count') do
|
||||
post games_url, params: {
|
||||
game: {
|
||||
title: "New Game",
|
||||
platform_id: platforms(:one).id,
|
||||
format: "physical"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_redirected_to game_path(Game.last)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Writing System Tests
|
||||
|
||||
```ruby
|
||||
# test/system/games_test.rb
|
||||
require "application_system_test_case"
|
||||
|
||||
class GamesTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
@user = users(:one)
|
||||
sign_in_as @user
|
||||
end
|
||||
|
||||
test "visiting the index" do
|
||||
visit games_url
|
||||
assert_selector "h1", text: "My Games"
|
||||
end
|
||||
|
||||
test "creating a game" do
|
||||
visit games_url
|
||||
click_on "Add Game"
|
||||
|
||||
fill_in "Title", with: "Test Game"
|
||||
select "Nintendo 64", from: "Platform"
|
||||
select "Physical", from: "Format"
|
||||
|
||||
click_on "Create Game"
|
||||
|
||||
assert_text "Game was successfully created"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Rails Console
|
||||
|
||||
```bash
|
||||
rails console
|
||||
|
||||
# Test queries
|
||||
User.first
|
||||
Game.where(format: :physical).count
|
||||
current_user.games.includes(:platform)
|
||||
|
||||
# Test RLS
|
||||
ActiveRecord::Base.connection.execute(
|
||||
"SET LOCAL app.current_user_id = 1"
|
||||
)
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# Tail development log
|
||||
tail -f log/development.log
|
||||
|
||||
# View specific log
|
||||
cat log/test.log
|
||||
```
|
||||
|
||||
### Debug with Byebug
|
||||
|
||||
Add to your code:
|
||||
```ruby
|
||||
require 'debug'
|
||||
debugger # Execution will pause here
|
||||
```
|
||||
|
||||
Then interact in the terminal:
|
||||
```
|
||||
n # Next line
|
||||
c # Continue
|
||||
p var # Print variable
|
||||
exit # Exit debugger
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Avoid N+1 Queries
|
||||
|
||||
Bad:
|
||||
```ruby
|
||||
@games = current_user.games
|
||||
# In view: @games.each { |g| g.platform.name } # N+1!
|
||||
```
|
||||
|
||||
Good:
|
||||
```ruby
|
||||
@games = current_user.games.includes(:platform)
|
||||
```
|
||||
|
||||
### Use Database Indexes
|
||||
|
||||
```ruby
|
||||
add_index :games, :title
|
||||
add_index :games, [:user_id, :platform_id]
|
||||
```
|
||||
|
||||
### Use Counter Caches
|
||||
|
||||
```ruby
|
||||
class Collection < ApplicationRecord
|
||||
has_many :games, counter_cache: true
|
||||
end
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```ruby
|
||||
@games = current_user.games.page(params[:page]).per(25)
|
||||
k8s/ # Kubernetes manifests
|
||||
docs/ # Documentation
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create `.env` for development (never commit!):
|
||||
```
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=postgres
|
||||
SMTP_ADDRESS=smtp.example.com
|
||||
SMTP_USERNAME=user@example.com
|
||||
SMTP_PASSWORD=secret
|
||||
```
|
||||
|
||||
Load with:
|
||||
```ruby
|
||||
# config/application.rb
|
||||
config.before_configuration do
|
||||
env_file = Rails.root.join('.env')
|
||||
if File.exist?(env_file)
|
||||
File.readlines(env_file).each do |line|
|
||||
key, value = line.split('=', 2)
|
||||
ENV[key.strip] = value.strip if key && value
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Kamal (Recommended)
|
||||
|
||||
Already configured! Just:
|
||||
Copy `.env.example` to `.env`:
|
||||
|
||||
```bash
|
||||
# First time setup
|
||||
kamal setup
|
||||
|
||||
# Deploy
|
||||
kamal deploy
|
||||
|
||||
# Check status
|
||||
kamal app exec --interactive --reuse "bin/rails console"
|
||||
# Optional: IGDB API
|
||||
IGDB_CLIENT_ID=your_client_id
|
||||
IGDB_CLIENT_SECRET=your_client_secret
|
||||
```
|
||||
|
||||
### Railway/Render
|
||||
Get credentials: https://dev.twitch.tv
|
||||
|
||||
1. Push to Git
|
||||
2. Connect repository
|
||||
3. Set environment variables
|
||||
4. Add build command: `bundle install && rails db:migrate`
|
||||
5. Add start command: `rails server -b 0.0.0.0`
|
||||
## Contributing
|
||||
|
||||
1. Fork repository
|
||||
2. Create feature branch
|
||||
3. Make changes
|
||||
4. Run: `task test && task lint:fix && task typecheck`
|
||||
5. Submit PR
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Connection Errors
|
||||
|
||||
```bash
|
||||
# Check if PostgreSQL is running
|
||||
docker compose ps
|
||||
# Database issues
|
||||
task docker:up && task db:reset
|
||||
|
||||
# Start PostgreSQL
|
||||
task docker:up
|
||||
# Type errors
|
||||
task tapioca:all && task typecheck
|
||||
|
||||
# Check database configuration
|
||||
cat config/database.yml
|
||||
# Tests failing
|
||||
task db:test:prepare && task test
|
||||
```
|
||||
|
||||
### Asset Issues
|
||||
|
||||
```bash
|
||||
# Rebuild assets
|
||||
rails assets:precompile
|
||||
|
||||
# Rebuild Tailwind
|
||||
rails tailwindcss:build
|
||||
```
|
||||
|
||||
### Migration Issues
|
||||
|
||||
```bash
|
||||
# Check migration status
|
||||
rails db:migrate:status
|
||||
|
||||
# Rollback and retry
|
||||
rails db:rollback
|
||||
rails db:migrate
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Rails Guides](https://guides.rubyonrails.org/)
|
||||
- [Rails API Documentation](https://api.rubyonrails.org/)
|
||||
- [Tailwind CSS Docs](https://tailwindcss.com/docs)
|
||||
- [Hotwire Documentation](https://hotwired.dev/)
|
||||
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. Check the logs: `tail -f log/development.log`
|
||||
2. Use Rails console: `rails console`
|
||||
3. Check database: `rails dbconsole`
|
||||
4. Read the error message carefully
|
||||
5. Search Stack Overflow
|
||||
6. Check Rails Guides
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always filter by current_user** in controllers
|
||||
2. **Use strong parameters** for mass assignment
|
||||
3. **Add validations** to models
|
||||
4. **Write tests** for new features
|
||||
5. **Keep controllers thin** - move logic to models
|
||||
6. **Use concerns** for shared code
|
||||
7. **Keep views simple** - use helpers for complex logic
|
||||
8. **Add indexes** for frequently queried columns
|
||||
9. **Use scopes** for common queries
|
||||
10. **Document your API** endpoints
|
||||
|
||||
Happy coding! 🚀
|
||||
|
||||
@@ -1,391 +1,114 @@
|
||||
# IGDB Integration Guide
|
||||
# IGDB Integration
|
||||
|
||||
## Overview
|
||||
|
||||
TurboVault now integrates with IGDB (Internet Game Database) to automatically match your games with their database entries. This provides access to cover art, metadata, and consistent game identification across users.
|
||||
TurboVault integrates with the [Internet Game Database (IGDB)](https://www.igdb.com/) to automatically enrich your games with metadata, cover art, and genre information.
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. **User Opt-In**
|
||||
- Users must enable IGDB sync in Settings
|
||||
- Default: OFF (privacy by design)
|
||||
- Can be toggled on/off anytime
|
||||
1. **Background sync** runs every 30 minutes (configurable)
|
||||
2. **Searches IGDB** for games without metadata
|
||||
3. **Creates suggestions** with confidence scores (0-100%)
|
||||
4. **Users review** and approve/reject matches
|
||||
5. **Metadata applied** when approved (cover art, genres, release dates)
|
||||
|
||||
### 2. **Automatic Sync Job**
|
||||
- Runs every 30 minutes
|
||||
- Only processes users with sync enabled
|
||||
- Matches games that don't have IGDB IDs yet
|
||||
## Setup
|
||||
|
||||
### 3. **Smart Matching**
|
||||
- Searches IGDB using game title + platform
|
||||
- Returns top 3 matches with confidence scores
|
||||
- Uses fuzzy matching and platform filtering
|
||||
### Get IGDB Credentials
|
||||
|
||||
### 4. **User Review**
|
||||
- Users review suggested matches
|
||||
- See cover art, release year, platform
|
||||
- Approve or reject each match
|
||||
- Even high-confidence matches require approval (prevents errors)
|
||||
1. Create account at https://dev.twitch.tv
|
||||
2. Register an application
|
||||
3. Copy Client ID and Client Secret
|
||||
|
||||
### 5. **Public Cache**
|
||||
- Matched games stored in `igdb_games` table
|
||||
- Shared across all users (public data only)
|
||||
- Reduces API calls for common games
|
||||
### Configure
|
||||
|
||||
## Features
|
||||
Add to `.env`:
|
||||
|
||||
### ✅ What's Implemented
|
||||
|
||||
1. **User Settings**
|
||||
- Enable/disable IGDB sync
|
||||
- Last sync timestamp
|
||||
|
||||
2. **Background Job**
|
||||
- Automatic recurring sync (every 30 minutes)
|
||||
- Rate limiting (4 req/sec)
|
||||
- Progress tracking
|
||||
- Error handling
|
||||
|
||||
3. **Smart Matching**
|
||||
- Title similarity scoring
|
||||
- Platform matching
|
||||
- Confidence calculation (0-100%)
|
||||
- Top 3 results per game
|
||||
|
||||
4. **Review Interface**
|
||||
- See all pending matches
|
||||
- View cover images
|
||||
- Approve/reject matches
|
||||
- Bulk reject option
|
||||
|
||||
5. **Public Game Cache**
|
||||
- Shared IGDB data
|
||||
- Cover URLs
|
||||
- Release dates
|
||||
- Match popularity tracking
|
||||
|
||||
6. **Platform Mappings**
|
||||
- 26 platforms mapped to IGDB IDs
|
||||
- Easy to extend
|
||||
|
||||
## Database Schema
|
||||
|
||||
### New Tables
|
||||
|
||||
**igdb_games** - Public cache of IGDB game data
|
||||
- igdb_id (unique)
|
||||
- name, slug, cover_url
|
||||
- summary, first_release_date
|
||||
- match_count (popularity)
|
||||
|
||||
**igdb_platform_mappings** - Maps our platforms to IGDB
|
||||
- platform_id → igdb_platform_id
|
||||
- Pre-seeded with 26 common platforms
|
||||
|
||||
**igdb_match_suggestions** - Pending matches for review
|
||||
- game_id + igdb_id
|
||||
- confidence_score
|
||||
- status (pending/approved/rejected)
|
||||
- Cover and metadata for review
|
||||
|
||||
### New Columns
|
||||
|
||||
**users**
|
||||
- igdb_sync_enabled (boolean)
|
||||
- igdb_last_synced_at (timestamp)
|
||||
|
||||
**games**
|
||||
- igdb_matched_at (timestamp)
|
||||
- igdb_match_status (string)
|
||||
- igdb_match_confidence (decimal)
|
||||
|
||||
## API Credentials
|
||||
|
||||
**Backend Only - Never Exposed to Frontend**
|
||||
|
||||
Set in `.env` (gitignored):
|
||||
```bash
|
||||
IGDB_CLIENT_ID=your_client_id
|
||||
IGDB_ACCESS_TOKEN=your_token
|
||||
IGDB_CLIENT_ID=your_client_id_here
|
||||
IGDB_CLIENT_SECRET=your_client_secret_here
|
||||
```
|
||||
|
||||
Get credentials from: https://api-docs.igdb.com/
|
||||
### Enable for Users
|
||||
|
||||
Current credentials are already set in the `.env` file.
|
||||
New users have IGDB sync enabled by default. Users can opt-out in Settings.
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
### Review Matches
|
||||
|
||||
1. **Enable Sync**
|
||||
- Go to Settings
|
||||
- Check "Enable IGDB game matching"
|
||||
- Click "Update Profile"
|
||||
Visit `/igdb_matches` to see pending suggestions.
|
||||
|
||||
2. **Trigger Sync**
|
||||
- Go to IGDB Matches page
|
||||
- Click "Sync Now"
|
||||
- Wait a few minutes
|
||||
Each suggestion shows:
|
||||
- Game title
|
||||
- Cover art
|
||||
- Platform
|
||||
- Confidence score
|
||||
- Genres from IGDB
|
||||
|
||||
3. **Review Matches**
|
||||
- View pending matches
|
||||
- See confidence scores
|
||||
- See cover art and release year
|
||||
- Approve correct matches
|
||||
- Reject incorrect ones
|
||||
**Actions:**
|
||||
- ✅ **Approve** - Apply metadata to your game
|
||||
- ❌ **Reject** - Dismiss suggestion
|
||||
- 🔄 **Sync Now** - Manually trigger sync for your games
|
||||
|
||||
4. **Check Progress**
|
||||
- View stats: Matched, Unmatched, Pending Review
|
||||
- See last sync time
|
||||
- Badge in navigation shows pending count
|
||||
### Manual Commands
|
||||
|
||||
### For Developers
|
||||
|
||||
**Manually Trigger Sync:**
|
||||
```ruby
|
||||
IgdbSyncJob.perform_later
|
||||
```
|
||||
|
||||
**Search IGDB Directly:**
|
||||
```ruby
|
||||
service = IgdbService.new
|
||||
results = service.search_game("Ocarina of Time", platform)
|
||||
```
|
||||
|
||||
**Check Platform Mappings:**
|
||||
```ruby
|
||||
IgdbPlatformMapping.igdb_id_for_platform(Platform.find_by(name: "Nintendo 64"))
|
||||
# => 4
|
||||
```
|
||||
|
||||
**Seed More Platform Mappings:**
|
||||
```ruby
|
||||
IgdbPlatformMapping.seed_common_mappings!
|
||||
```
|
||||
|
||||
## Match Confidence Scoring
|
||||
|
||||
Confidence score is 0-100%:
|
||||
|
||||
**Title Matching (0-70 points)**
|
||||
- Exact match: 70 points
|
||||
- Contains match: 50 points
|
||||
- Word overlap: 0-40 points
|
||||
|
||||
**Platform Matching (0-30 points)**
|
||||
- Exact platform: 30 points
|
||||
- Similar platform: 20 points
|
||||
- No match: 0 points
|
||||
|
||||
**Result Categories:**
|
||||
- 95-100%: Very High (auto-suggest first)
|
||||
- 70-94%: High Confidence
|
||||
- 50-69%: Medium Confidence
|
||||
- 0-49%: Low Confidence
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
**IGDB Limits:** 4 requests/second
|
||||
|
||||
**Our Implementation:**
|
||||
- 0.3s sleep between requests
|
||||
- 1s sleep every 10 games
|
||||
- Handles 429 errors gracefully
|
||||
- Job can be safely interrupted
|
||||
|
||||
## Job Scheduling
|
||||
|
||||
**Recurring Schedule:**
|
||||
- Every 30 minutes
|
||||
- Configured in `config/queue.yml`
|
||||
- Uses Solid Queue
|
||||
|
||||
**Single Instance:**
|
||||
- Uses Rails cache lock
|
||||
- Prevents multiple instances running
|
||||
- 2-hour timeout (auto-releases)
|
||||
|
||||
**Manual Trigger:**
|
||||
- "Sync Now" button in UI
|
||||
- Or: `IgdbSyncJob.perform_later`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Matches Found
|
||||
|
||||
**Possible Reasons:**
|
||||
1. Game title too different from IGDB
|
||||
2. Platform not mapped
|
||||
3. Game not in IGDB database
|
||||
|
||||
**Solutions:**
|
||||
- Check game title spelling
|
||||
- Try alternate titles
|
||||
- Check if platform is mapped
|
||||
- Some games may not be in IGDB
|
||||
|
||||
### Rate Limit Errors
|
||||
|
||||
If you see 429 errors:
|
||||
- Job will automatically pause
|
||||
- Will resume on next run
|
||||
- Check IGDB API status
|
||||
|
||||
### Job Not Running
|
||||
|
||||
**Check:**
|
||||
```ruby
|
||||
# Is job scheduled?
|
||||
SolidQueue::RecurringTask.all
|
||||
|
||||
# Is job running?
|
||||
IgdbSyncJob.running?
|
||||
|
||||
# Clear lock if stuck
|
||||
Rails.cache.delete("igdb_sync_job:running")
|
||||
```
|
||||
|
||||
### Matches Not Appearing
|
||||
|
||||
**Check:**
|
||||
1. Is sync enabled for user?
|
||||
2. Are there unmatched games?
|
||||
3. Check logs: `tail -f log/development.log`
|
||||
4. Run manually: `IgdbSyncJob.perform_now`
|
||||
|
||||
## Adding New Platform Mappings
|
||||
|
||||
```ruby
|
||||
# Find IGDB platform ID from: https://api-docs.igdb.com/#platform
|
||||
platform = Platform.find_by(name: "Your Platform")
|
||||
IgdbPlatformMapping.create!(
|
||||
platform: platform,
|
||||
igdb_platform_id: 123, # IGDB ID
|
||||
igdb_platform_name: "Platform Name"
|
||||
)
|
||||
```
|
||||
|
||||
## Cover Images
|
||||
|
||||
**IGDB CDN URLs:**
|
||||
```ruby
|
||||
# In model:
|
||||
igdb_game.cover_image_url("cover_big")
|
||||
# => https://images.igdb.com/igdb/image/upload/t_cover_big/abc123.jpg
|
||||
|
||||
# Available sizes:
|
||||
# - cover_small (90x128)
|
||||
# - cover_big (264x374)
|
||||
# - screenshot_med (569x320)
|
||||
# - screenshot_big (1280x720)
|
||||
# - screenshot_huge (1920x1080)
|
||||
# - 720p, 1080p
|
||||
```
|
||||
|
||||
## Privacy & Security
|
||||
|
||||
**What's Shared:**
|
||||
- Only IGDB game data (names, covers, dates)
|
||||
- NO user-specific data (prices, locations, notes)
|
||||
- `igdb_games` table is public metadata only
|
||||
|
||||
**What's Private:**
|
||||
- User's game ownership
|
||||
- Collections
|
||||
- Personal notes, prices, locations
|
||||
- Which user matched which game
|
||||
|
||||
**API Credentials:**
|
||||
- Stored in ENV variables
|
||||
- Never sent to frontend
|
||||
- Backend-only service class
|
||||
|
||||
## Performance
|
||||
|
||||
**Typical Sync:**
|
||||
- ~100 games: 2-3 minutes
|
||||
- ~500 games: 10-15 minutes
|
||||
- Rate limited for API safety
|
||||
|
||||
**Database:**
|
||||
- Indexes on all foreign keys
|
||||
- Cache lookup before API calls
|
||||
- Efficient batch processing
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
**Phase 2 Ideas:**
|
||||
- Auto-approve very high confidence (>95%)
|
||||
- Bulk approve/reject
|
||||
- Search IGDB directly from Add Game form
|
||||
- Download and store cover images locally
|
||||
- More metadata (genres, ratings, descriptions)
|
||||
- Manual IGDB search for failed matches
|
||||
- Game recommendations based on IGDB data
|
||||
|
||||
## API Documentation
|
||||
|
||||
**IGDB API Docs:** https://api-docs.igdb.com/
|
||||
|
||||
**Authentication:**
|
||||
- Requires Twitch Client ID + Access Token
|
||||
- Token must be refreshed periodically (check expiry)
|
||||
|
||||
**Endpoints Used:**
|
||||
- `POST /v4/games` - Search and fetch game data
|
||||
|
||||
**Query Example:**
|
||||
```
|
||||
search "Zelda Ocarina";
|
||||
fields id, name, slug, cover.url, summary, first_release_date, platforms.name;
|
||||
where platforms = (4);
|
||||
limit 3;
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
**Test the Flow:**
|
||||
1. Enable sync in settings
|
||||
2. Add a game without IGDB match
|
||||
3. Click "Sync Now"
|
||||
4. Wait 1-2 minutes
|
||||
5. Refresh page - should see matches
|
||||
6. Approve or reject matches
|
||||
|
||||
**Test Games:**
|
||||
- "The Legend of Zelda: Ocarina of Time" (N64) - Should find match
|
||||
- "Super Mario 64" (N64) - Should find match
|
||||
- "Made Up Game Name" - Should find no results
|
||||
|
||||
## Support
|
||||
|
||||
**Check Logs:**
|
||||
```bash
|
||||
tail -f log/development.log | grep IGDB
|
||||
```
|
||||
# Trigger sync manually
|
||||
task igdb:sync
|
||||
|
||||
**Rails Console:**
|
||||
```ruby
|
||||
# Check sync status
|
||||
User.find_by(email: "demo@turbovault.com").igdb_sync_enabled
|
||||
task igdb:status
|
||||
|
||||
# View pending matches
|
||||
User.first.igdb_match_suggestions.status_pending
|
||||
|
||||
# Test IGDB service
|
||||
service = IgdbService.new
|
||||
results = service.search_game("Mario 64", Platform.find_by(abbreviation: "N64"))
|
||||
# Clear stuck jobs
|
||||
task igdb:clear
|
||||
```
|
||||
|
||||
## Summary
|
||||
## How Matching Works
|
||||
|
||||
The IGDB integration is now fully functional:
|
||||
- ✅ User opt-in settings
|
||||
- ✅ Automatic sync every 30 minutes
|
||||
- ✅ Smart matching with confidence scores
|
||||
- ✅ Review UI with cover art
|
||||
- ✅ Public game cache
|
||||
- ✅ Rate limiting and error handling
|
||||
- ✅ Privacy-preserving design
|
||||
**Confidence scoring:**
|
||||
- Title match: 0-70 points
|
||||
- Platform match: 0-30 points
|
||||
- 100% = exact title + platform match
|
||||
|
||||
Users can now match their games with IGDB for better organization and future features like cover art display!
|
||||
**Fallback search:**
|
||||
- First tries with platform filter
|
||||
- Falls back to no platform if no results
|
||||
|
||||
**Genre mapping:**
|
||||
IGDB genres are automatically mapped to your local genres (e.g., "Role-playing (RPG)" → "RPG").
|
||||
|
||||
## Platform Support
|
||||
|
||||
26 platforms supported including:
|
||||
- Nintendo (NES, SNES, N64, GameCube, Wii, Switch)
|
||||
- PlayStation (PS1-PS5, PSP, Vita)
|
||||
- Xbox (Xbox, 360, One, Series X/S)
|
||||
- Sega (Genesis, Dreamcast, Saturn)
|
||||
- PC platforms (Steam, Epic, GOG)
|
||||
|
||||
## Background Job
|
||||
|
||||
**Schedule:** Every 30 minutes
|
||||
**Configuration:** `config/queue.yml`
|
||||
**Job:** `IgdbSyncJob`
|
||||
|
||||
The job:
|
||||
- Only processes users with `igdb_sync_enabled: true`
|
||||
- Skips games with `igdb_id` already set
|
||||
- Creates up to 3 match suggestions per game
|
||||
- Respects rate limits (4 requests/second)
|
||||
|
||||
## API Rate Limits
|
||||
|
||||
- **Limit:** 4 requests per second
|
||||
- **Handled:** Automatic 0.3s delay between requests
|
||||
- **Caching:** Games cached in `igdb_games` table
|
||||
|
||||
## Disabling IGDB
|
||||
|
||||
Users can disable in Settings → "Enable IGDB metadata sync"
|
||||
|
||||
Admins can disable globally by removing env vars or stopping the recurring job.
|
||||
|
||||
## Attribution
|
||||
|
||||
IGDB attribution link in footer (required by IGDB terms).
|
||||
|
||||
@@ -1,305 +1,50 @@
|
||||
# 🚀 TurboVault Quick Start Guide
|
||||
# Quick Start Guide
|
||||
|
||||
Get TurboVault deployed to Kubernetes in minutes!
|
||||
Get TurboVault running locally in minutes.
|
||||
|
||||
## Prerequisites
|
||||
## Local Development
|
||||
|
||||
- GitHub account
|
||||
- Kubernetes cluster (k3s, minikube, EKS, GKE, etc.)
|
||||
- kubectl configured
|
||||
- PostgreSQL database (or use in-cluster Helm chart)
|
||||
### Prerequisites
|
||||
|
||||
## Step 1: Push to GitHub
|
||||
- Ruby 3.3+
|
||||
- Docker (for services)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd turbovault-web
|
||||
# Clone
|
||||
git clone https://github.com/ryankazokas/turbovault-app.git
|
||||
cd turbovault
|
||||
|
||||
# Initialize git
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: TurboVault"
|
||||
# Start services (PostgreSQL + Mailpit)
|
||||
task docker:up
|
||||
|
||||
# Add your GitHub remote
|
||||
git remote add origin https://github.com/YOUR_USERNAME/turbovault.git
|
||||
git push -u origin main
|
||||
# Setup database
|
||||
task db:setup
|
||||
|
||||
# Start server
|
||||
task dev
|
||||
```
|
||||
|
||||
## Step 2: Tag a Release
|
||||
Visit **http://localhost:3000**
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
### Demo Account
|
||||
|
||||
**What happens:**
|
||||
- GitHub Actions automatically triggers
|
||||
- Builds Docker image
|
||||
- Pushes to GitHub Container Registry (ghcr.io)
|
||||
- Image: `ghcr.io/YOUR_USERNAME/turbovault:v1.0.0`
|
||||
- Email: `demo@turbovault.com`
|
||||
- Password: `password123`
|
||||
|
||||
**Check progress:** GitHub → Actions tab
|
||||
## Optional: IGDB Integration
|
||||
|
||||
## Step 3: Prepare Kubernetes Secrets
|
||||
For automatic game metadata:
|
||||
|
||||
```bash
|
||||
# Copy the template
|
||||
cp k8s/secrets.yaml.example k8s/secrets.yaml
|
||||
1. Create app at https://dev.twitch.tv
|
||||
2. Add to `.env`:
|
||||
```bash
|
||||
IGDB_CLIENT_ID=your_id
|
||||
IGDB_CLIENT_SECRET=your_secret
|
||||
```
|
||||
3. Restart server
|
||||
|
||||
# Generate Rails secret
|
||||
rails secret
|
||||
# Copy output
|
||||
## Deployment
|
||||
|
||||
# Edit secrets.yaml
|
||||
nano k8s/secrets.yaml
|
||||
```
|
||||
|
||||
Add your values:
|
||||
- `SECRET_KEY_BASE` - from `rails secret` command
|
||||
- `DATABASE_PASSWORD` - your PostgreSQL password
|
||||
- `IGDB_CLIENT_ID` (optional) - from https://dev.twitch.tv
|
||||
- `IGDB_CLIENT_SECRET` (optional) - from Twitch developer portal
|
||||
|
||||
**Important:** Do NOT commit `k8s/secrets.yaml` (it's gitignored)
|
||||
|
||||
## Step 4: Update Kubernetes Manifests
|
||||
|
||||
Edit `k8s/deployment.yaml` and `k8s/migrate-job.yaml`:
|
||||
|
||||
```yaml
|
||||
# Change this line:
|
||||
image: ghcr.io/username/turbovault:latest
|
||||
|
||||
# To your actual GitHub username:
|
||||
image: ghcr.io/YOUR_USERNAME/turbovault:v1.0.0
|
||||
```
|
||||
|
||||
Edit `k8s/configmap.yaml`:
|
||||
|
||||
```yaml
|
||||
DATABASE_HOST: "your-postgres-host" # e.g., postgres-service or external host
|
||||
DATABASE_NAME: "turbovault_production"
|
||||
DATABASE_USERNAME: "turbovault"
|
||||
```
|
||||
|
||||
Edit `k8s/ingress.yaml` (optional):
|
||||
|
||||
```yaml
|
||||
# Change to your domain
|
||||
host: turbovault.yourdomain.com
|
||||
```
|
||||
|
||||
Or skip ingress and use port-forwarding for testing.
|
||||
|
||||
## Step 5: Deploy to Kubernetes
|
||||
|
||||
### Option A: Automated Script
|
||||
|
||||
```bash
|
||||
./scripts/deploy-k8s.sh
|
||||
```
|
||||
|
||||
Follow the prompts:
|
||||
- Enter registry: `ghcr.io/YOUR_USERNAME`
|
||||
- Is it private? `n` (if image is public) or `y` (if private)
|
||||
- Deployment starts!
|
||||
|
||||
### Option B: Manual
|
||||
|
||||
```bash
|
||||
# Apply manifests
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
kubectl apply -f k8s/configmap.yaml
|
||||
kubectl apply -f k8s/secrets.yaml
|
||||
|
||||
# Run database migration
|
||||
kubectl apply -f k8s/migrate-job.yaml
|
||||
kubectl wait --for=condition=complete --timeout=300s job/turbovault-migrate -n turbovault
|
||||
|
||||
# Deploy application
|
||||
kubectl apply -f k8s/deployment.yaml
|
||||
kubectl apply -f k8s/service.yaml
|
||||
kubectl apply -f k8s/ingress.yaml # optional
|
||||
```
|
||||
|
||||
## Step 6: Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check pods
|
||||
kubectl get pods -n turbovault
|
||||
|
||||
# Should show:
|
||||
# NAME READY STATUS RESTARTS AGE
|
||||
# turbovault-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
|
||||
# turbovault-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
|
||||
|
||||
# Check logs
|
||||
kubectl logs -f deployment/turbovault -n turbovault
|
||||
```
|
||||
|
||||
## Step 7: Access the Application
|
||||
|
||||
### Option A: Via Ingress (if configured)
|
||||
|
||||
Visit: `https://turbovault.yourdomain.com`
|
||||
|
||||
### Option B: Port Forward (for testing)
|
||||
|
||||
```bash
|
||||
kubectl port-forward svc/turbovault-service 3000:80 -n turbovault
|
||||
```
|
||||
|
||||
Visit: `http://localhost:3000`
|
||||
|
||||
### Option C: LoadBalancer (cloud)
|
||||
|
||||
```bash
|
||||
kubectl get svc turbovault-service -n turbovault
|
||||
|
||||
# Get EXTERNAL-IP and visit that IP
|
||||
```
|
||||
|
||||
## Step 8: Create Admin Account
|
||||
|
||||
1. Visit the application
|
||||
2. Click **Sign Up**
|
||||
3. Create your account
|
||||
4. Start adding games!
|
||||
|
||||
## 🎉 You're Done!
|
||||
|
||||
TurboVault is now running on Kubernetes!
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Make it Public
|
||||
|
||||
Want others to access your package?
|
||||
|
||||
1. Go to GitHub → Your Profile → Packages
|
||||
2. Find `turbovault` package
|
||||
3. Package Settings → Change Visibility → Public
|
||||
|
||||
### Keep it Private
|
||||
|
||||
By default, GitHub Container Registry packages are private. Only you can pull the image. Perfect for personal deployments!
|
||||
|
||||
For Kubernetes to pull private images:
|
||||
|
||||
```bash
|
||||
kubectl create secret docker-registry ghcr-secret \
|
||||
--docker-server=ghcr.io \
|
||||
--docker-username=YOUR_USERNAME \
|
||||
--docker-password=YOUR_GITHUB_TOKEN \
|
||||
--namespace=turbovault
|
||||
```
|
||||
|
||||
Then uncomment in `k8s/deployment.yaml`:
|
||||
```yaml
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
```
|
||||
|
||||
### Add SSL/TLS
|
||||
|
||||
Install cert-manager and configure Let's Encrypt:
|
||||
|
||||
See [DEPLOYMENT.md](DEPLOYMENT.md) for full instructions.
|
||||
|
||||
### Update the App
|
||||
|
||||
When you want to deploy a new version:
|
||||
|
||||
```bash
|
||||
# Make changes
|
||||
git add .
|
||||
git commit -m "Add new feature"
|
||||
git push origin main
|
||||
|
||||
# Create new tag
|
||||
git tag v1.1.0
|
||||
git push origin v1.1.0
|
||||
|
||||
# Wait for GitHub Actions to build
|
||||
|
||||
# Update deployment
|
||||
kubectl set image deployment/turbovault \
|
||||
turbovault=ghcr.io/YOUR_USERNAME/turbovault:v1.1.0 \
|
||||
-n turbovault
|
||||
|
||||
# Watch rollout
|
||||
kubectl rollout status deployment/turbovault -n turbovault
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pods not starting
|
||||
|
||||
```bash
|
||||
kubectl describe pod -l app=turbovault -n turbovault
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- Image pull error → Check image path in deployment.yaml
|
||||
- Database connection → Check secrets.yaml and configmap.yaml
|
||||
- Crash loop → Check logs: `kubectl logs -l app=turbovault -n turbovault`
|
||||
|
||||
### Can't access application
|
||||
|
||||
```bash
|
||||
# Check service
|
||||
kubectl get svc -n turbovault
|
||||
|
||||
# Check ingress (if using)
|
||||
kubectl get ingress -n turbovault
|
||||
|
||||
# Try port-forward to test
|
||||
kubectl port-forward svc/turbovault-service 3000:80 -n turbovault
|
||||
```
|
||||
|
||||
### Build failed
|
||||
|
||||
Go to GitHub → Actions → Click on failed workflow
|
||||
|
||||
Common issues:
|
||||
- Dockerfile error → Fix and push again
|
||||
- Permission denied → Check workflow has `packages: write` permission
|
||||
|
||||
## Database Setup
|
||||
|
||||
### Option 1: External PostgreSQL (Recommended)
|
||||
|
||||
Use a managed database (RDS, Cloud SQL, etc.) or existing PostgreSQL server.
|
||||
|
||||
Update `k8s/configmap.yaml` with connection details.
|
||||
|
||||
### Option 2: In-Cluster PostgreSQL
|
||||
|
||||
```bash
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
helm install postgres bitnami/postgresql \
|
||||
--namespace turbovault \
|
||||
--set auth.database=turbovault_production \
|
||||
--set auth.username=turbovault \
|
||||
--set auth.password=changeme
|
||||
|
||||
# Update k8s/configmap.yaml:
|
||||
# DATABASE_HOST: postgres-postgresql
|
||||
```
|
||||
|
||||
## Complete Documentation
|
||||
|
||||
- [Full Deployment Guide](DEPLOYMENT.md) - Detailed deployment instructions
|
||||
- [Development Guide](DEVELOPMENT_GUIDE.md) - Local development setup
|
||||
- [API Documentation](API_DOCUMENTATION.md) - RESTful API reference
|
||||
- [IGDB Integration](IGDB_INTEGRATION.md) - Game metadata matching
|
||||
|
||||
## Support
|
||||
|
||||
Need help?
|
||||
- 📖 Check the [docs/](.) folder
|
||||
- 🐛 [Open an issue](https://github.com/yourusername/turbovault/issues)
|
||||
- 💬 [Discussions](https://github.com/yourusername/turbovault/discussions)
|
||||
|
||||
---
|
||||
|
||||
**Congratulations!** You've successfully deployed TurboVault to Kubernetes! 🎉
|
||||
For production deployment to Kubernetes, see [Deployment Guide](DEPLOYMENT.md).
|
||||
|
||||
@@ -1,45 +1,14 @@
|
||||
# TurboVault Documentation
|
||||
|
||||
Complete documentation for TurboVault - Video Game Collection Tracker
|
||||
## Quick Links
|
||||
|
||||
## 📖 Documentation
|
||||
- [Quick Start](QUICK_START.md) - Get up and running
|
||||
- [Development](DEVELOPMENT_GUIDE.md) - Local development
|
||||
- [Deployment](DEPLOYMENT.md) - Kubernetes deployment
|
||||
- [API Reference](API_DOCUMENTATION.md) - RESTful API
|
||||
- [IGDB Integration](IGDB_INTEGRATION.md) - Automatic metadata
|
||||
|
||||
### Getting Started
|
||||
- [Main README](../README.md) - Project overview
|
||||
- ⭐ [Quick Start Guide](QUICK_START.md) - **Deploy in minutes!**
|
||||
- [Demo Account](DEMO_ACCOUNT.md) - Try the demo
|
||||
## Demo
|
||||
|
||||
### Deployment & Development
|
||||
- [Deployment Guide](DEPLOYMENT.md) - Complete deployment reference
|
||||
- [Development Guide](DEVELOPMENT_GUIDE.md) - Local development & contributing
|
||||
- [CI Pipeline](CI_PIPELINE.md) - GitHub Actions & quality checks
|
||||
- [Kubernetes README](../k8s/README.md) - Kubernetes deployment
|
||||
|
||||
### Features & Development
|
||||
- [API Documentation](API_DOCUMENTATION.md) - RESTful API reference
|
||||
- [IGDB Integration](IGDB_INTEGRATION.md) - Game metadata matching
|
||||
- [Themes](THEMES.md) - Theme customization
|
||||
- [Sorbet Type Checking](SORBET.md) - Gradual static types
|
||||
|
||||
### Configuration
|
||||
- [GitHub Secrets Setup](../.github/SECRETS_SETUP.md) - Optional custom registry
|
||||
- [What to Commit](../.github/WHAT_TO_COMMIT.md) - Safe for open source
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Navigation
|
||||
|
||||
**New to TurboVault?**
|
||||
1. Read [Main README](../README.md)
|
||||
2. Deploy with [Quick Start Guide](QUICK_START.md) ⭐
|
||||
3. Or develop locally with [Development Guide](DEVELOPMENT_GUIDE.md)
|
||||
|
||||
**Looking for something specific?**
|
||||
- Deploying to production → [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
- Using the API → [API_DOCUMENTATION.md](API_DOCUMENTATION.md)
|
||||
- Game metadata features → [IGDB_INTEGRATION.md](IGDB_INTEGRATION.md)
|
||||
- Customizing themes → [THEMES.md](THEMES.md)
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Check the [Main README](../README.md) or [open an issue](https://github.com/yourusername/turbovault/issues).
|
||||
Email: `demo@turbovault.com`
|
||||
Password: `password123`
|
||||
|
||||
520
docs/SORBET.md
520
docs/SORBET.md
@@ -1,520 +0,0 @@
|
||||
# Sorbet Type Checking Guide
|
||||
|
||||
TurboVault uses [Sorbet](https://sorbet.org/) for gradual static type checking.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### First Time Setup
|
||||
|
||||
After installing gems, initialize Sorbet:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bundle install
|
||||
|
||||
# Initialize Tapioca (generates type definitions)
|
||||
task tapioca:init
|
||||
|
||||
# Generate RBI files for gems and Rails DSLs
|
||||
task tapioca:all
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `sorbet/` - Sorbet configuration
|
||||
- `sorbet/rbi/gems/` - Type definitions for gems (gitignored)
|
||||
- `sorbet/rbi/dsl/` - Generated types for Rails models, etc. (gitignored)
|
||||
|
||||
### Running Type Checks
|
||||
|
||||
```bash
|
||||
# Check all files
|
||||
task typecheck
|
||||
|
||||
# Watch mode (re-checks on file changes)
|
||||
task typecheck:watch
|
||||
|
||||
# Or use Sorbet directly
|
||||
bundle exec srb tc
|
||||
```
|
||||
|
||||
## Type Strictness Levels
|
||||
|
||||
Sorbet uses gradual typing with 5 strictness levels:
|
||||
|
||||
### `# typed: false` (Default - No Checking)
|
||||
```ruby
|
||||
# typed: false
|
||||
class MyClass
|
||||
def do_something(x)
|
||||
x + 1 # No type checking at all
|
||||
end
|
||||
end
|
||||
```
|
||||
- **Use for:** Legacy code, external gems, code you're not ready to type yet
|
||||
- **Checking:** None
|
||||
|
||||
### `# typed: true` (Recommended)
|
||||
```ruby
|
||||
# typed: true
|
||||
class User < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(String) }
|
||||
def theme_class
|
||||
"theme-#{theme}"
|
||||
end
|
||||
end
|
||||
```
|
||||
- **Use for:** Most application code
|
||||
- **Checking:** Method signatures you define with `sig`
|
||||
|
||||
### `# typed: strict` (Advanced)
|
||||
- **Use for:** Critical business logic, libraries
|
||||
- **Checking:** All method signatures required, no untyped code
|
||||
|
||||
### `# typed: strong` (Expert)
|
||||
- **Use for:** Maximum safety
|
||||
- **Checking:** No `T.untyped`, no runtime checks
|
||||
|
||||
## Writing Type Signatures
|
||||
|
||||
### Basic Method Signatures
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
# No parameters, returns String
|
||||
sig { returns(String) }
|
||||
def title
|
||||
read_attribute(:title)
|
||||
end
|
||||
|
||||
# One parameter, returns Boolean
|
||||
sig { params(user: User).returns(T::Boolean) }
|
||||
def owned_by?(user)
|
||||
self.user_id == user.id
|
||||
end
|
||||
|
||||
# Multiple parameters
|
||||
sig { params(title: String, platform: Platform).returns(Game) }
|
||||
def self.create_game(title, platform)
|
||||
create(title: title, platform: platform)
|
||||
end
|
||||
|
||||
# Void (returns nothing meaningful)
|
||||
sig { void }
|
||||
def update_cache
|
||||
Rails.cache.write("game_#{id}", self)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Nilable Types
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
# Can return Platform or nil
|
||||
sig { returns(T.nilable(Platform)) }
|
||||
def platform
|
||||
super
|
||||
end
|
||||
|
||||
# Parameter can be nil
|
||||
sig { params(platform_id: T.nilable(Integer)).returns(T.untyped) }
|
||||
def self.by_platform(platform_id)
|
||||
where(platform_id: platform_id) if platform_id
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Arrays and Hashes
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
# Array of Hashes
|
||||
sig { params(title: String).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
||||
def search_game(title)
|
||||
# Returns: [{id: 1, name: "Game"}, {id: 2, name: "Game 2"}]
|
||||
end
|
||||
|
||||
# Array of specific type
|
||||
sig { returns(T::Array[Game]) }
|
||||
def all_games
|
||||
Game.all.to_a
|
||||
end
|
||||
|
||||
# Hash with specific keys
|
||||
sig { returns(T::Hash[String, Integer]) }
|
||||
def game_counts
|
||||
{ "total" => 100, "completed" => 50 }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Optional Parameters
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
# Optional parameter with default
|
||||
sig { params(title: String, limit: Integer).returns(T::Array[T.untyped]) }
|
||||
def search_game(title, limit = 3)
|
||||
# ...
|
||||
end
|
||||
|
||||
# Optional keyword parameter
|
||||
sig { params(title: String, platform: T.nilable(String)).returns(T::Array[T.untyped]) }
|
||||
def search(title, platform: nil)
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Instance Variables
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def initialize
|
||||
@client_id = T.let(ENV.fetch("IGDB_CLIENT_ID"), String)
|
||||
@client_secret = T.let(ENV.fetch("IGDB_CLIENT_SECRET"), String)
|
||||
@access_token = T.let(get_or_refresh_token, String)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`T.let(value, Type)` declares the type of an instance variable.
|
||||
|
||||
### Untyped Code
|
||||
|
||||
When you can't determine the type (ActiveRecord relations, dynamic code):
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class GamesController < ApplicationController
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def index
|
||||
# ActiveRecord relations are complex, use T.untyped
|
||||
@games = T.let(current_user.games.includes(:platform), T.untyped)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Use `T.untyped` sparingly - it bypasses type checking.
|
||||
|
||||
### CSV Rows (Common Pattern)
|
||||
|
||||
CSV::Row acts like a Hash but Sorbet doesn't know that:
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
sig { void }
|
||||
def bulk_create
|
||||
csv = CSV.parse(csv_text, headers: true)
|
||||
|
||||
csv.each_with_index do |row, index|
|
||||
# Tell Sorbet row is untyped to avoid Array vs Hash confusion
|
||||
row = T.let(row, T.untyped)
|
||||
|
||||
# Now you can access row["column"] without errors
|
||||
title = row["title"]
|
||||
platform = row["platform"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This is acceptable because CSV parsing is inherently dynamic and validated by your model.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Controllers
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class GamesController < ApplicationController
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def index
|
||||
@games = T.let(current_user.games.all, T.untyped)
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def show
|
||||
@game = T.let(current_user.games.find(params[:id]), Game)
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def create
|
||||
@game = T.let(current_user.games.build(game_params), Game)
|
||||
if @game.save
|
||||
redirect_to @game
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Models
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
# Class methods
|
||||
sig { params(query: String).returns(T.untyped) }
|
||||
def self.search(query)
|
||||
where("title ILIKE ?", "%#{query}%")
|
||||
end
|
||||
|
||||
# Instance methods
|
||||
sig { returns(String) }
|
||||
def display_title
|
||||
"#{title} (#{platform&.name || 'Unknown'})"
|
||||
end
|
||||
|
||||
# Private methods
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def set_defaults
|
||||
self.date_added ||= Date.current
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def initialize
|
||||
@client_id = T.let(ENV.fetch("IGDB_CLIENT_ID"), String)
|
||||
@access_token = T.let(get_token, String)
|
||||
end
|
||||
|
||||
sig { params(title: String, platform: T.nilable(String)).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
||||
def search_game(title, platform: nil)
|
||||
results = post("/games", build_query(title, platform))
|
||||
format_results(results)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { params(title: String, platform: T.nilable(String)).returns(String) }
|
||||
def build_query(title, platform)
|
||||
"search \"#{title}\";"
|
||||
end
|
||||
|
||||
sig { params(results: T::Array[T.untyped]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
||||
def format_results(results)
|
||||
results.map { |r| { id: r["id"], name: r["name"] } }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Gradual Adoption Strategy
|
||||
|
||||
### Phase 1: Infrastructure (Current)
|
||||
- ✅ Add Sorbet gems
|
||||
- ✅ Generate RBI files
|
||||
- ✅ Add `# typed: true` to key files
|
||||
- ✅ Type critical methods (User, Game, IgdbService)
|
||||
|
||||
### Phase 2: Core Models (Next)
|
||||
- Add types to all models
|
||||
- Type associations and scopes
|
||||
- Type validations and callbacks
|
||||
|
||||
### Phase 3: Controllers
|
||||
- Add types to controller actions
|
||||
- Type params and filters
|
||||
- Type helper methods
|
||||
|
||||
### Phase 4: Services & Jobs
|
||||
- Type all service objects
|
||||
- Type background jobs
|
||||
- Type API clients
|
||||
|
||||
### Phase 5: Increase Strictness
|
||||
- Move files from `typed: true` to `typed: strict`
|
||||
- Remove `T.untyped` where possible
|
||||
- Add runtime type checks where needed
|
||||
|
||||
## IDE Integration
|
||||
|
||||
### VSCode
|
||||
|
||||
Install the [Sorbet extension](https://marketplace.visualstudio.com/items?itemName=sorbet.sorbet-vscode-extension):
|
||||
|
||||
1. Install extension
|
||||
2. Sorbet will auto-detect your project
|
||||
3. Get inline type errors and autocomplete
|
||||
|
||||
### RubyMine
|
||||
|
||||
Sorbet support is built-in:
|
||||
|
||||
1. Enable Sorbet in Settings → Languages & Frameworks → Ruby
|
||||
2. RubyMine will use Sorbet for type checking
|
||||
|
||||
## Updating Types
|
||||
|
||||
When you add gems or change models:
|
||||
|
||||
```bash
|
||||
# After adding/updating gems
|
||||
task tapioca:gems
|
||||
|
||||
# After changing models/routes/etc
|
||||
task tapioca:dsl
|
||||
|
||||
# Both at once
|
||||
task tapioca:all
|
||||
```
|
||||
|
||||
Run this after:
|
||||
- `bundle install` (new/updated gems)
|
||||
- Adding/changing models
|
||||
- Adding/changing routes
|
||||
- Adding/changing concerns
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Parent of class redefined" (RDoc, etc.)
|
||||
|
||||
```
|
||||
Parent of class RDoc::Markup::Heading redefined from RDoc::Markup::Element to Struct
|
||||
```
|
||||
|
||||
**Fix:** This is a known gem incompatibility. Already fixed in `sorbet/config`:
|
||||
```
|
||||
--suppress-payload-superclass-redefinition-for=RDoc::Markup::Heading
|
||||
```
|
||||
|
||||
If you see similar errors for other gems, add suppression flags to `sorbet/config`.
|
||||
|
||||
### "Method does not exist"
|
||||
|
||||
```
|
||||
app/models/game.rb:10: Method `foo` does not exist on `Game`
|
||||
```
|
||||
|
||||
**Fix:** Add the method signature or check for typos.
|
||||
|
||||
### "Argument does not have asserted type"
|
||||
|
||||
```
|
||||
Expected String but got T.nilable(String)
|
||||
```
|
||||
|
||||
**Fix:** Handle nil case or use `T.must()`:
|
||||
|
||||
```ruby
|
||||
sig { params(name: String).void }
|
||||
def greet(name)
|
||||
puts "Hello #{name}"
|
||||
end
|
||||
|
||||
# Wrong
|
||||
greet(user.name) # name might be nil
|
||||
|
||||
# Right
|
||||
greet(user.name || "Guest")
|
||||
greet(T.must(user.name)) # Asserts non-nil
|
||||
```
|
||||
|
||||
### "Constants must have type annotations"
|
||||
|
||||
```ruby
|
||||
# Wrong
|
||||
MY_CONSTANT = "value"
|
||||
|
||||
# Right
|
||||
MY_CONSTANT = T.let("value", String)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
- Start with `# typed: true` (not strict)
|
||||
- Use `sig` for public methods
|
||||
- Type critical business logic
|
||||
- Use `T.untyped` for complex ActiveRecord queries
|
||||
- Update RBI files after gem/model changes
|
||||
- Run type checks in CI
|
||||
|
||||
### ❌ DON'T:
|
||||
- Type everything at once (gradual adoption!)
|
||||
- Use `# typed: strict` on new code (too strict)
|
||||
- Ignore type errors (fix or suppress with comment)
|
||||
- Forget to run `tapioca:dsl` after model changes
|
||||
|
||||
## CI Integration
|
||||
|
||||
Add to `.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
- name: Type check with Sorbet
|
||||
run: |
|
||||
bundle exec tapioca gems --no-doc
|
||||
bundle exec tapioca dsl
|
||||
bundle exec srb tc
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Sorbet Documentation](https://sorbet.org/)
|
||||
- [Sorbet Playground](https://sorbet.run/)
|
||||
- [Tapioca Documentation](https://github.com/Shopify/tapioca)
|
||||
- [T::Types Cheat Sheet](https://sorbet.org/docs/stdlib-generics)
|
||||
|
||||
## Current Status
|
||||
|
||||
**All files start with `# typed: false`** (no checking enabled yet).
|
||||
|
||||
**Ready to type (after running setup):**
|
||||
- `app/models/user.rb`
|
||||
- `app/models/game.rb`
|
||||
- `app/services/igdb_service.rb`
|
||||
- `app/controllers/games_controller.rb`
|
||||
|
||||
**Recommended typing order:**
|
||||
1. Start with Models: User, Game, Platform, Genre
|
||||
2. Then Services: IgdbService
|
||||
3. Then Controllers: GamesController, CollectionsController
|
||||
4. Finally Jobs and Helpers
|
||||
|
||||
**Important:** Run the setup steps first:
|
||||
```bash
|
||||
bundle install
|
||||
task tapioca:init
|
||||
task tapioca:all
|
||||
task typecheck # Should show "No errors!"
|
||||
```
|
||||
|
||||
Then start adding `# typed: true` and signatures incrementally.
|
||||
|
||||
---
|
||||
|
||||
**Happy typing!** 🎉 Start small, add types gradually, and enjoy better IDE support and bug catching!
|
||||
@@ -1,66 +0,0 @@
|
||||
# TurboVault Themes
|
||||
|
||||
## Available Themes
|
||||
|
||||
TurboVault now supports **5 beautiful themes** to customize your experience!
|
||||
|
||||
### 🎨 Theme Gallery
|
||||
|
||||
#### ☀️ Light (Default)
|
||||
- Clean, bright interface
|
||||
- Easy on the eyes during daytime
|
||||
- Classic Tailwind styling
|
||||
|
||||
#### 🌙 Dark
|
||||
- Modern dark mode
|
||||
- Reduced eye strain in low light
|
||||
- Sleek and professional
|
||||
|
||||
#### 🌃 Midnight
|
||||
- Deep blue tones
|
||||
- Perfect for late-night gaming sessions
|
||||
- Calm and immersive
|
||||
|
||||
#### 🕹️ Retro
|
||||
- Classic gaming aesthetic
|
||||
- Brown and gold color scheme
|
||||
- Nostalgic vibes
|
||||
|
||||
#### 🌊 Ocean
|
||||
- Blue and teal theme
|
||||
- Fresh and vibrant
|
||||
- Aquatic inspiration
|
||||
|
||||
## How to Change Your Theme
|
||||
|
||||
1. Go to **Settings** page
|
||||
2. Scroll to **Theme** section
|
||||
3. Click on your preferred theme card
|
||||
4. Click **"Update Profile"**
|
||||
5. Enjoy your new look! 🎉
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Themes are stored per-user in the database
|
||||
- Applied via CSS classes on the `<body>` tag
|
||||
- Works seamlessly with Turbo (no page reload needed)
|
||||
- Default theme: `light`
|
||||
|
||||
## Adding New Themes
|
||||
|
||||
To add a new theme:
|
||||
|
||||
1. Add validation in `app/models/user.rb`
|
||||
2. Add theme styles in `app/assets/stylesheets/themes.css`
|
||||
3. Add theme option in `app/views/users/settings.html.erb`
|
||||
|
||||
## Theme Persistence
|
||||
|
||||
Your theme preference is saved to your account and will persist across:
|
||||
- Different browsers
|
||||
- Different devices
|
||||
- App restarts
|
||||
|
||||
## Browser Support
|
||||
|
||||
Themes work in all modern browsers that support CSS custom properties and Tailwind CSS.
|
||||
Reference in New Issue
Block a user