Files
oslstats/AGENTS.md
2026-02-13 13:27:14 +11:00

356 lines
9.3 KiB
Markdown

# AGENTS.md - Developer Guide for oslstats
This document provides guidelines for AI coding agents and developers working on the oslstats codebase.
## Project Overview
**Module**: `git.haelnorr.com/h/oslstats`
**Language**: Go 1.25.5
**Architecture**: Web application with Discord OAuth, PostgreSQL database, templ templates
**Key Technologies**: Bun ORM, templ, TailwindCSS, custom golib libraries
## Build and Development Commands
### Building
NEVER BUILD MANUALLY
```bash
# Full production build (tailwind → templ → go generate → go build)
just build
# Build and run
just run
```
### Development Mode
```bash
# Watch mode with hot reload (templ, air, tailwindcss in parallel)
just dev
# Development server runs on:
# - Proxy: http://localhost:3000 (use this)
# - App: http://localhost:3333 (internal)
```
### Database Migrations
**oslstats uses Bun's migration framework for safe, incremental schema changes.**
#### Quick Reference
**New Migration System**: Migrations now accept a count parameter. Default is 1 migration at a time.
```bash
# Show migration status
just migrate status
# Run 1 migration (default, with automatic backup)
just migrate up 1
# OR just
just migrate up
# Run 3 migrations
just migrate up 3
# Run all pending migrations
just migrate up all
# Run with a specific environment file
just migrate up 3 .test.env
# Rollback works the same for all arguments
just migrate down 2 .test.env
# Create new migration
just migrate new add_email_to_users
# Dev: Reset database (DESTRUCTIVE - deletes all data)
just reset-db
```
#### Creating a New Migration
**Example: Adding an email field to users table**
1. **Generate migration file:**
```bash
just migrate new add_leagues_and_slap_version
```
Creates: `cmd/oslstats/migrations/20250124150030_add_leagues_and_slap_version.go`
2. **Edit the migration file:**
```go
package migrations
import (
"context"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(
// UP: Add email column
func(ctx context.Context, db *bun.DB) error {
_, err := dbConn.NewAddColumn().
Model((*db.Season)(nil)).
ColumnExpr("slap_version VARCHAR NOT NULL").
IfNotExists().
Exec(ctx)
if err != nil {
return err
}
// Create leagues table
_, err = dbConn.NewCreateTable().
Model((*db.League)(nil)).
Exec(ctx)
if err != nil {
return err
}
// Create season_leagues join table
_, err = dbConn.NewCreateTable().
Model((*db.SeasonLeague)(nil)).
Exec(ctx)
return err
},
// DOWN: Remove email column (for rollback)
func(ctx context.Context, db *bun.DB) error {
// Drop season_leagues join table first
_, err := dbConn.NewDropTable().
Model((*db.SeasonLeague)(nil)).
IfExists().
Exec(ctx)
if err != nil {
return err
}
// Drop leagues table
_, err = dbConn.NewDropTable().
Model((*db.League)(nil)).
IfExists().
Exec(ctx)
if err != nil {
return err
}
// Remove slap_version column from seasons table
_, err = dbConn.NewDropColumn().
Model((*db.Season)(nil)).
ColumnExpr("slap_version").
Exec(ctx)
return err
},
)
}
```
3. **Update the model** (`internal/db/user.go`):
```go
type Season struct {
bun.BaseModel `bun:"table:seasons,alias:s"`
ID int `bun:"id,pk,autoincrement"`
Name string `bun:"name,unique"`
SlapVersion string `bun:"slap_version"` // NEW FIELD
}
```
4. **Apply the migration:**
```bash
just migrate up 1
```
Output:
```
[INFO] Step 1/5: Validating migrations...
[INFO] Migration validation passed ✓
[INFO] Step 2/5: Checking for pending migrations...
[INFO] Running 1 migration(s):
📋 20250124150030_add_email_to_users
[INFO] Step 3/5: Creating backup...
[INFO] Backup created: backups/20250124_150145_pre_migration.sql.gz (2.3 MB)
[INFO] Step 4/5: Acquiring migration lock...
[INFO] Migration lock acquired
[INFO] Step 5/5: Applying migrations...
[INFO] Migrated to group 2
✅ 20250124150030_add_email_to_users
[INFO] Migration lock released
```
#### Environment Variables
```bash
# Backup directory (default: backups)
DB_BACKUP_DIR=backups
# Number of backups to keep (default: 10)
DB_BACKUP_RETENTION=10
```
#### Troubleshooting
**"pg_dump not found"**
- Migrations will still run, but backups will be skipped
- Install PostgreSQL client tools for backups:
```bash
# Ubuntu/Debian
sudo apt-get install postgresql-client
# macOS
brew install postgresql
# Arch
sudo pacman -S postgresql-libs
```
**"migration already in progress"**
- Another instance is running migrations
- Wait for it to complete (max 5 minutes)
- If stuck, check for hung database connections
**"migration build failed"**
- Migration file has syntax errors
- Fix the errors and try again
- Use `go build ./cmd/oslstats/migrations` to debug
### Configuration Management
```bash
# Generate .env template file
just genenv
# OR with custom output: just genenv .env.example
# Show environment variable documentation
just envdoc
# Show current environment values
just showenv
```
## Code Style Guidelines
### Error Handling
**Always wrap errors** with context using `github.com/pkg/errors`:
```go
if err != nil {
return errors.Wrap(err, "package.FunctionName")
}
```
**Validate inputs at function start**:
```go
func DoSomething(cfg *Config, data string) error {
if cfg == nil {
return errors.New("cfg cannot be nil")
}
if data == "" {
return errors.New("data cannot be empty")
}
// ... rest of function
}
```
**HTTP error helpers** (in handlers package):
*Note: `msg` should be a frontend presentable message*
- `throwInternalServiceError(s, w, r, msg, err)` - 500 errors
- `throwBadRequest(s, w, r, msg, err)` - 400 errors
- `throwForbidden(s, w, r, msg, err)` - 403 errors (normal)
- `throwForbiddenSecurity(s, w, r, msg, err)` - 403 security violations (WARN level)
- `throwUnauthorized(s, w, r, msg, err)` - 401 errors (normal)
- `throwUnauthorizedSecurity(s, w, r, msg, err)` - 401 security violations (WARN level)
- `throwNotFound(s, w, r, path)` - 404 errors
### Common Patterns
**HTTP Handler Pattern**:
```go
func HandlerName(server *hws.Server, deps ...) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Handler logic here
},
)
}
```
**Database Operation Pattern**:
```go
func GetSomething(ctx context.Context, tx bun.Tx, id int) (*Result, error) {
result := new(Result)
err := tx.NewSelect().
Model(result).
Where("id = ?", id).
Scan(ctx)
if err != nil {
if err.Error() == "sql: no rows in result set" {
return nil, nil // Return nil, nil for not found
}
return nil, errors.Wrap(err, "tx.Select")
}
return result, nil
}
```
**Setup Function Pattern** (returns instance, cleanup func, error):
```go
func setupSomething(ctx context.Context, cfg *Config) (*Type, func() error, error) {
instance := newInstance()
err := configure(instance)
if err != nil {
return nil, nil, errors.Wrap(err, "configure")
}
return instance, instance.Close, nil
}
```
**Configuration Pattern** (using ezconf):
```go
type Config struct {
Field string // ENV FIELD_NAME: Description (required/default: value)
}
func ConfigFromEnv() (any, error) {
cfg := &Config{
Field: env.String("FIELD_NAME", "default"),
}
// Validation here
return cfg, nil
}
```
### Formatting & Types
**Formatting**:
- Use `gofmt` (standard Go formatting)
**Types**:
- Prefer explicit types over inference when it improves clarity
- Use struct tags for ORM and JSON marshaling:
```go
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID int `bun:"id,pk,autoincrement"`
Username string `bun:"username,unique"`
AccessToken string `json:"access_token"`
}
```
**Comments**:
- Document exported functions and types
- Use inline comments for ENV var documentation in Config structs
- Explain security-critical code flows
## Notes for AI Agents
1. **Never commit** .env files, keys/, or generated files (*_templ.go, output.css)
2. **Database operations** should use `bun.Tx` for transaction safety
3. **Templates** are written in templ, not Go html/template - run `just templ` after changes
4. **Static files** are embedded via `//go:embed` - check internal/embedfs/
5. **Error messages** should be descriptive and use errors.Wrap for context
6. **Security is critical** - especially in OAuth flows (see pkg/oauth/state_test.go for examples)
7. **Air proxy** runs on port 3000 during development; app runs on 3333
8. **Configuration** uses ezconf pattern - see internal/*/ezconf.go files for examples
9. When in plan mode, always use the interactive question tool if available