Repository guidelines for AI coding agents (Codex, Claude Code, etc.) working on qui.
- Do not implement review-suggested or extra changes outside requested scope without explicit user approval first.
- Treat other agent/Codex/CodeRabbit feedback as input to discuss, not automatic action.
The Go backend lives in cmd/qui (entrypoint) and internal/ modules for configuration, qBittorrent, metrics, and API routing; shared helpers sit in pkg/. The React/Vite client is in web/src with static assets in web/public, and its production bundle must stay synced to internal/web/dist. Reference docs live under docs/, while Docker and compose files in the repository root support container workflows.
End-user docs live in the Docusaurus project under documentation/docs/. Prefer updating those for user-facing copy; docs/ is mostly internal/engineering notes.
Keep README.md concise. Feature deep-dives belong in documentation/docs/, not the root README.
# Build
make build # Frontend bundle + Go binary with version metadata
make backend # Go binary only
make frontend # Frontend bundle only
# Development
make dev # Starts air (hot-reload) + pnpm dev
make dev-backend # Backend only with hot-reload
make dev-frontend # Frontend only
# Testing
make test # go test -race -count=1 -v ./...
make test-openapi # Validate OpenAPI spec after touching internal/web/swagger
# Linting
make lint # Changed files only (fast, use during iteration)
make lint-json # JSON output to lint-report.json
# Pre-commit
make precommit # fmt + gofix + lint on changed files
# Formatting
make fmt # gofmt + frontend eslint --fix on changed files
make gofix-changed # Apply go fix on changed Go files only
make gofix-check-changed # Check go fix drift on changed Go files onlyThe project uses golangci-lint v2 with strict configuration targeting AI-generated code patterns:
| Linter | Purpose | Threshold |
|---|---|---|
| dupl | Catch code duplication | 100 tokens |
| gocognit | Cognitive complexity | 15 |
| funlen | Function length | 80 lines |
| interfacebloat | Interface size | 5 methods |
| errcheck | Unchecked errors | All, including type assertions |
| gocritic | Non-idiomatic patterns | diagnostic + style + performance |
Workflow:
- During implementation:
make precommit(changed files only, fast feedback) - To fix issues:
make lint-fixthen address remaining manually
Guardrail (web formatting): avoid repo-wide pnpm format / eslint --fix sweeps unless explicitly requested. Prefer fixing only the files reported by lint for the current task/PR.
Keep Go code gofmt-clean with PascalCase exports, camelCase locals, and package-level interfaces grouped by domain inside internal/<area>. The frontend follows ESLint @stylistic defaults: two-space indentation, double quotes, trailing commas on multiline literals, and Unix line endings. Organize React modules by feature within web/src/{pages,routes,components} and choose descriptive file names (e.g., torrent-table.tsx).
Critical conventions:
- Prefer explicit error handling over silent failures
- Keep interfaces small (≤5 methods)
- Avoid
map[string]interface{}— use proper structs - No backward compatibility shims unless explicitly requested
- Loop variables (Go 1.22+): Don't use
tt := ttin parallel subtests — Go 1.22+ creates a new variable per iteration, so the old workaround is unnecessary and flagged by linters
Single-user self-hosted context: qui runs on someone's home server, not as a multi-tenant SaaS with untrusted input and complex failure modes. Skip paranoid defensive programming for impossible or purely theoretical scenarios. Code that guards against states that can't happen adds complexity without value. Prioritize readable, maintainable code over excessive robustness.
- Prefer behavior-bearing branches only. If multiple
switchcases return the same value asdefault, collapse them. - In boolean classifiers, list only the exceptional cases (
truecases or error cases). Letdefaulthandle the common path. - Do not add documentation-only branches unless they enforce something mechanically via compiler, linter, or tests.
- When a branch only enumerates known states, ask whether it changes behavior, improves safety, or provides exhaustiveness checking. If not, delete it.
- Use
useEffectonly to sync with external systems (DOM, subscriptions, network). - Avoid derived state in Effects; calculate during render, or
useMemofor expensive compute. - Put user-driven logic in event handlers, not Effects.
- To reset state, prefer a
keyor render-time adjustments instead of Effects. - Fetch Effects must guard against stale responses (cleanup/abort).
- Source: https://react.dev/learn/you-might-not-need-an-effect
Place backend tests beside implementations as *_test.go, mirroring paths such as internal/qbittorrent/pool_test.go. Prefer table-driven cases and reuse the integration fixtures already in internal/qbittorrent/. Run targeted local tests for touched packages and add make test-openapi when contracts change. CI covers the full make test suite unless explicitly requested locally. Frontend work should include Vitest + React Testing Library specs named *.test.tsx near the component.
When running tests, always use -race and -count=1.
For changes under internal/services/crossseed or internal/qbittorrent, run targeted package tests first. Skip local full make test by default; CI covers it unless explicitly requested.
When adding Go tests that create files with os.WriteFile, use 0o600 or tighter permissions unless the test explicitly needs broader mode bits. This avoids gosec G306 lint failures.
Follow the conventional commit style in history (feat(scope):, fix(scope):, etc.) and link issues or PR numbers in the body when relevant. Keep commits focused—split backend and frontend changes when practical.
Never add:
- "🤖 Generated with Claude Code"
- "Co-Authored-By: Claude" or any AI co-author credits
- Any advertising or attribution in commit messages
PRs need a clear summary, testing checklist, and UI screenshots for visual tweaks. Confirm local targeted verification, make lint, and a fresh make build succeed before requesting review; rely on CI for the full make test suite unless explicitly requested.
make precommitpasses (fmt+gofix-changed+lint, changed files only)- Targeted local tests for touched packages pass (full
make testcovered by CI unless explicitly requested) make buildsucceeds- If touched
internal/web/swagger, runmake test-openapi
Load secrets such as THEMES_REPO_TOKEN via .env so the Makefile can fetch premium themes, and keep the file out of version control. Record configuration defaults in config.toml but evolve runtime schema through Go migrations rather than editing qui.db directly. Drop cached databases and logs (qui.db*, logs/) from commits to avoid leaking local data.
- Database schema changes must ship as migrations under
internal/database/migrations, include matching model/store updates in the same PR, and add both SQLite and Postgres migrations. - For an open PR, keep schema work consolidated to at most one new SQLite migration and one new Postgres migration. If the PR needs more schema changes before merge, edit the draft migration files for that PR instead of adding more migration files.
- API contract changes must update OpenAPI content under
internal/web/swaggerand passmake test-openapi. - Prefer minimal, reviewable diffs in high-churn areas (
internal/services/crossseed,internal/qbittorrent,internal/models).
cmd/qui/main.go CLI entrypoint (serve, generate-config, create-user, etc.)
internal/api/ HTTP handlers + middleware (chi router)
internal/qbittorrent/ Client pool, sync manager
internal/services/ Domain services (crossseed, jackett, reannounce, trackerrules)
internal/proxy/ Reverse proxy for external apps
internal/backups/ Scheduled snapshots
internal/database/ SQLite + migrations
internal/models/ Data models + store interfaces
pkg/ Shared utilities
web/src/ React 19 + Vite + TypeScript + Tailwind v4
Key data flow:
SyncManagerpolls qBittorrent instances viaClientPool- Torrent state cached in-memory with delta updates
- Frontend fetches via REST API, real-time updates via SSE
- Cross-seed service listens for torrent completion events