# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build, Test, and Development Commands

This is a .NET 10 solution using the modern XML-based `.slnx` format. The global SDK version is `10.0.100` with `rollForward: latestFeature`.

**Build the solution:**
```bash
dotnet build
```

**Build individual projects (the CI does this to include SAST via SecurityCodeScan):**
```bash
dotnet build src/GmRelay.Shared/GmRelay.Shared.csproj --no-restore
dotnet build src/GmRelay.Bot/GmRelay.Bot.csproj --no-restore
dotnet build src/GmRelay.Web/GmRelay.Web.csproj --no-restore
```

**Run all tests:**
```bash
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --verbosity normal
```

**Run a single test class or method:**
```bash
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --filter "FullyQualifiedName~YourTestClassName"
```

**Lint and format:**
```bash
dotnet format --verify-no-changes --verbosity diagnostic   # CI enforcement
dotnet format                                                  # Apply fixes
```

**Check for vulnerable packages:**
```bash
dotnet list package --vulnerable --include-transitive
```

**Restore with lock file verification:**
The repo enforces `RestorePackagesWithLockFile=true`. After adding or updating packages, commit the updated `packages.lock.json` files or the Trivy scan in CI will fail.

**Run locally with Aspire (dev orchestration):**
```bash
dotnet run --project src/GmRelay.AppHost/GmRelay.AppHost.csproj
```
This automatically starts PostgreSQL in a container, the Bot, and the Web dashboard.

**Run locally with Docker Compose (production-like):**
```bash
cp .env.example .env
# Edit .env with your TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_USERNAME, POSTGRES_PASSWORD
docker compose up -d
```

## High-Level Architecture

### Project Roles and Runtime Model

| Project | Runtime | Key Trait |
|---|---|---|
| `GmRelay.Bot` | `Microsoft.NET.Sdk.Worker` | **Native AOT** binary. Telegram long polling bot + stateless scheduler. |
| `GmRelay.Web` | `Microsoft.NET.Sdk.Web` | Blazor Server dashboard. Cookie auth via Telegram Login Widget / Mini App `initData`. |
| `GmRelay.Shared` | Plain library | Domain models and platform-neutral view builders. **Must not depend on `Telegram.Bot`**. |
| `GmRelay.ServiceDefaults` | Aspire shared project | OpenTelemetry, health checks, HTTP resilience. Referenced by both Bot and Web. |
| `GmRelay.AppHost` | Aspire orchestrator | Dev-only. Spins up PostgreSQL and wires Bot + Web with service discovery. |

**Important:** `README.md` references `GmRelay.Migrator` and `GmRelay.Worker`, but these projects do not exist. Migrations (`DbUp`) and background workers (`BackgroundService`) live inside `GmRelay.Bot`.

### Vertical Slice Architecture with Explicit DI

Each use case is a self-contained vertical slice: a C# record (Command/Query) + Handler class with all logic (SQL, Telegram API calls, validation). There are no abstract repository interfaces or service layers.

Because the Bot is compiled as Native AOT (`PublishAot=true`, `EnableTrimAnalyzer=true`), **all DI registrations are explicit** in `src/GmRelay.Bot/Program.cs`. There is no assembly scanning or reflection-based discovery. When adding a new handler, you must register it manually in Program.cs.

### Database Access: Npgsql + Dapper.AOT + DbUp

**No EF Core** — it is incompatible with Native AOT. The stack is:
- **Npgsql** ADO.NET for connections.
- **Dapper 2.1.72** with **Dapper.AOT 1.0.48** for compile-time source-generated mapping (AOT-safe).
- **DbUp 7.0.1** for migrations. SQL scripts are embedded resources in `src/GmRelay.Bot/Migrations/` (V001 through V015).
- `DbMigrator.MigrateUp()` runs on every Bot startup.

Both Bot and Web share the same PostgreSQL database. Web registers `NpgsqlDataSource` via `builder.AddNpgsqlDataSource("gmrelaydb")` (Aspire integration), while Bot registers it manually to avoid reflection-based Aspire configuration at AOT time.

### Platform-Neutral Rendering (ADR-002)

Rendering is split into two stages:
1. **View Builder** (`GmRelay.Shared`) — platform-agnostic view model from domain DTOs.
2. **Platform Renderer** — `TelegramSessionBatchRenderer` lives in both `GmRelay.Bot` and `GmRelay.Web` (temporary duplication until a third Telegram consumer justifies extracting `GmRelay.Shared.Telegram`).

This means `GmRelay.Shared` must remain free of `Telegram.Bot` types. If you need to add rendering logic that produces `InlineKeyboardMarkup`, it belongs in the Bot or Web project, not Shared.

### Stateless Scheduling

The session scheduler (`SessionSchedulerService`) is a `BackgroundService` with a `PeriodicTimer(TimeSpan.FromMinutes(1))`. On each tick it queries PostgreSQL for sessions needing action (T-24h confirmation, T-5min join link) and updates their status. There is no in-memory state — the database is the single source of truth. This design was chosen specifically because Quartz.NET is incompatible with Native AOT.

### Health Checks

- **Bot:** Custom `BotHealthCheckHostedService` listens on port 8081. The Docker health check hits `localhost:8081/health`.
- **Web:** Standard ASP.NET Core health checks on `/health` (JSON response with status and timestamp) and `/alive` (liveness probe tag filter). Exposed via `GmRelay.ServiceDefaults`.

### Authentication and Security

- **Telegram Login Widget** and **Mini App `initData`** verification via HMAC-SHA256. Cookie auth is hardened (`HttpOnly`, `SecurePolicy.Always`, `SameSite.Strict`).
- Web Data Protection keys are persisted to `/app/dataprotection-keys` (Docker volume `web_keys`).
- Security headers middleware (`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`) is applied globally in Web.
- `SecurityCodeScan.VS2019` (5.6.7) is included in all projects via `Directory.Build.props` for SAST at build time.
- Connection string passwords are redacted in logs via `SecretRedactor`.

### CI/CD Pipeline

`.gitea/workflows/pr-checks.yml` runs on every PR to `main`:
1. `dotnet restore`
2. Verify `packages.lock.json` files exist for Trivy
3. `dotnet format --verify-no-changes`
4. `dotnet list package --vulnerable`
5. Trivy filesystem scan (`vuln,misconfig,secret`, HIGH/CRITICAL)
6. Build Shared → Bot → Web
7. Run tests

`.gitea/workflows/deploy.yml` runs on push to `main`:
1. Build and push `gmrelay-bot` and `gmrelay-web` images to `git.codeanddice.ru/toutsu/...`
2. Trivy image scan on both images (HIGH/CRITICAL, exit-code 1)
3. Create `.env` from secrets and run `docker compose up -d`

### Environment Configuration

Key environment variables (see `.env.example`):
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_USERNAME`, `TELEGRAM_MINI_APP_URL`
- `POSTGRES_PASSWORD`
- `GMRELAY_WEB_PORT` (default 8080)
- `ConnectionStrings__gmrelaydb` — used by both Bot and Web

The Bot reads config as `Telegram:BotToken` (colon) which maps from `Telegram__BotToken` (double underscore) via environment variables.

### Docker Images

- **Bot:** Multi-stage Dockerfile. Build stage uses `sdk:10.0-noble` with `clang` and `zlib1g-dev` for AOT compilation. Final stage uses `runtime-deps:10.0-noble`. Exposes 8081.
- **Web:** Multi-stage Dockerfile. Build stage uses `sdk:10.0-noble`. Final stage uses `aspnet:10.0-noble` with `libgssapi-krb5-2` and `wget`. Exposes 8080.

Both images are built for multi-arch (`linux/amd64`, `linux/arm64`) to support Raspberry Pi 5 (ARM64) deployment.
