# 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, Test, and Development Commands ### Building ```bash # Full production build (tailwind → templ → go generate → go build) make build # Build and run make run # Clean build artifacts make clean ``` ### Development Mode ```bash # Watch mode with hot reload (templ, air, tailwindcss in parallel) make dev # Development server runs on: # - Proxy: http://localhost:3000 (use this) # - App: http://localhost:3333 (internal) ``` ### Testing ```bash # Run all tests go test ./... # Run tests for a specific package go test ./pkg/oauth # Run a single test function go test ./pkg/oauth -run TestGenerateState_Success # Run tests with verbose output go test -v ./pkg/oauth # Run tests with coverage go test -cover ./... go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out ``` ### Database ```bash # Run migrations make migrate # OR ./bin/oslstats --migrate ``` ### Configuration Management ```bash # Generate .env template file make genenv # OR with custom output: make genenv OUT=.env.example # Show environment variable documentation make envdoc # Show current environment values make showenv ``` ## Code Style Guidelines ### Import Organization Organize imports in **3 groups** separated by blank lines: ```go import ( // 1. Standard library "context" "net/http" "fmt" // 2. External dependencies "git.haelnorr.com/h/golib/hws" "github.com/pkg/errors" "github.com/uptrace/bun" // 3. Internal packages "git.haelnorr.com/h/oslstats/internal/config" "git.haelnorr.com/h/oslstats/pkg/oauth" ) ``` ### Naming Conventions **Variables**: - Local: `camelCase` (userAgentKey, httpServer, dbConn) - Exported: `PascalCase` (Config, User, Token) - Common abbreviations: `cfg`, `ctx`, `tx`, `db`, `err`, `w`, `r` **Functions**: - Exported: `PascalCase` (GetConfig, NewStore, GenerateState) - Private: `camelCase` (throwError, shouldShowDetails, loadModels) - HTTP handlers: Return `http.Handler`, use dependency injection pattern - Database functions: Use `bun.Tx` as parameter for transactions **Types**: - Structs/Interfaces: `PascalCase` (Config, User, OAuthSession) - Use `-er` suffix for interfaces (implied from usage) **Files**: - Prefer single word: `config.go`, `oauth.go`, `errors.go` - Use snake_case if needed: `discord_tokens.go`, `state_test.go` - Test files: `*_test.go` alongside source files ### Error Handling **Always wrap errors** with context using `github.com/pkg/errors`: ```go if err != nil { return errors.Wrap(err, "operation_name") } ``` **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): - `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) - No tabs vs spaces debate - Go uses tabs **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 ### Testing **Test File Location**: Place `*_test.go` files alongside source files **Test Naming**: ```go func TestFunctionName_Scenario(t *testing.T) func TestGenerateState_Success(t *testing.T) func TestVerifyState_WrongUserAgentKey(t *testing.T) ``` **Test Structure**: - Use subtests with `t.Run()` for related scenarios - Use table-driven tests for multiple similar cases - Create helper functions for common setup (e.g., `testConfig()`) - Test happy paths, error cases, edge cases, and security properties **Test Categories** (from pkg/oauth/state_test.go example): 1. Happy path tests 2. Error handling (nil params, empty fields, malformed input) 3. Security tests (MITM, CSRF, replay attacks, tampering) 4. Edge cases (concurrency, constant-time comparison) 5. Integration tests (round-trip verification) ### Security **Critical Practices**: - Use `crypto/subtle.ConstantTimeCompare` for cryptographic comparisons - Implement CSRF protection via state tokens - Store sensitive cookies as HttpOnly - Use separate logging levels for security violations (WARN) - Validate all inputs at function boundaries - Use parameterized queries (Bun ORM handles this) - Never commit secrets (.env, keys/ are gitignored) ## Project Structure ``` oslstats/ ├── cmd/oslstats/ # Application entry point │ ├── main.go # Entry point with flag parsing │ ├── run.go # Server initialization & graceful shutdown │ ├── httpserver.go # HTTP server setup │ ├── routes.go # Route registration │ ├── middleware.go # Middleware registration │ ├── auth.go # Authentication setup │ └── db.go # Database connection & migrations ├── internal/ # Private application code │ ├── config/ # Configuration aggregation │ ├── db/ # Database models & queries (Bun ORM) │ ├── discord/ # Discord OAuth integration │ ├── handlers/ # HTTP request handlers │ ├── session/ # Session store (in-memory) │ └── view/ # Templ templates │ ├── component/ # Reusable UI components │ ├── layout/ # Page layouts │ └── page/ # Full pages ├── pkg/ # Reusable packages │ ├── contexts/ # Context key definitions │ ├── embedfs/ # Embedded static files │ └── oauth/ # OAuth state management ├── bin/ # Compiled binaries (gitignored) ├── keys/ # Private keys (gitignored) ├── tmp/ # Air hot reload temp files (gitignored) ├── Makefile # Build automation ├── .air.toml # Hot reload configuration └── go.mod # Go module definition ``` ## Key Dependencies - **git.haelnorr.com/h/golib/*** - Custom libraries (env, ezconf, hlog, hws, hwsauth, cookies, jwt) - **github.com/a-h/templ** - Type-safe HTML templating - **github.com/uptrace/bun** - PostgreSQL ORM - **github.com/bwmarrin/discordgo** - Discord API client - **github.com/pkg/errors** - Error wrapping (use this, not fmt.Errorf) - **github.com/joho/godotenv** - .env file loading ## 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 `templ generate` after changes 4. **Static files** are embedded via `//go:embed` - check pkg/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. **Test coverage** is currently limited - prioritize testing security-critical code 9. **Configuration** uses ezconf pattern - see internal/*/ezconf.go files for examples 10. **Graceful shutdown** is implemented in cmd/oslstats/run.go - follow this pattern 11. When in plan mode, always use the interactive question tool if available