# myAi — Solution Guide

## Infrastructure URLs

| Purpose | URL |
|---------|-----|
| Staging app | https://myai.easysoft.ro |
| Production app | https://myai.ro |
| Portainer (container management) | https://portainer.easysoft.ro/#!/auth |
| Grafana (logs) | https://grafana.easysoft.ro/login |
| Gitea (source control) | https://git.easysoft.ro |

The Gitea instance has two deployment repos:
- **staging repo** → auto-deploys to `myai.easysoft.ro`
- **production repo** → auto-deploys to `myai.ro`

## Staging browser testing

To verify a feature against staging use the `verify` skill pointed at `https://myai.easysoft.ro`.  
Portainer at `portainer.easysoft.ro` can restart containers or inspect live state.  
Grafana at `grafana.easysoft.ro` shows structured logs from all containers.

## Feature workflow (plan → ship)

When a plan is approved and implementation begins:
1. Add the plan as a **Gitea Wiki page** in the relevant repository (under a `Features/` or `Plans/` namespace)
2. Create **Gitea Issues** — one per logical work chunk — and link them to the Wiki page
3. Reference the issue number in commit messages (`Closes #N`)
4. Issues are closed automatically (or manually) when the code is merged

This applies to both the staging and production repos as appropriate.

## Tech stack
- .NET 10, ASP.NET Core, Worker Service
- Entity Framework Core + SQL Server (multi-schema)
- Refit for typed HTTP clients between services
- Serilog — Compact JSON logs to stdout (+ optional email sink); see Observability
- MailKit for SMTP (used exclusively in `email-api`)
- Docker Compose for local and production deployment
- Watchtower for automatic container updates in production

## Observability (central stack on monitoring host 10.0.0.156)
- **Logs**: every service uses `ConfigureJsonSerilog(ServiceName, appVersion)` (startup-helpers) → Serilog **Compact JSON** to stdout, enriched `Application`/`Environment`/`AppVersion`. The host's Grafana **Alloy** agent ships stdout → **Loki**; view/query in Grafana. No file sink; optional email sink only if `SerilogEmail:*` is configured.
- **No app metrics/traces** — these are simple/minimal services, so (unlike easyDent) they don't expose Prometheus metrics or OTLP traces. Container/host metrics still come from the host's cAdvisor/node_exporter.

## Project taxonomy

| Category | Naming | Contains | EF dependency |
|----------|--------|----------|---------------|
| Executable | `{name}-api`, `{name}-job` | Controllers, Services, Program.cs | Via `ProjectReference` to a `-data` project |
| Domain contracts | `{name}-models`, `{name}-api-models`, `{name}-job-models` | DTOs, Refit interfaces, domain-specific Settings | No |
| Data layer | `{name}-data` | DbContext, EF entities, Migrations | Yes |
| Common contracts | `common` (no suffix) | Infrastructure/technical primitives — no domain ownership | No |
| Common base entities | `shared-data` | Abstract `BaseEntity` class (Id + CreatedAt). No DbContext. | No |

### The `common` project rule

`common` holds **only infrastructure/technical primitives** with no specific service domain ownership: `DatabaseSettings`, `InternalApiSettings`, `ErrorResponse`, `RateLimitingSettings`, `UploadFileRequest`, AI provider settings, etc. **Never put a business-domain type in `common`** — domain types belong in the owning service's `-models` project.

### Where migrations live

**Migrations always live in the `-data` project**, never in an API or Job project. EF CLI split: `--project` = `-data` project (owns the schema); `--startup-project` = whichever API supplies the DB connection string.

## Solution layout

```
Apis/
  api/                    Public-facing proxy API (port 8080). Handles CORS, rate limiting, captcha, email.
  api-models/             DTOs and settings for api only.
  email-api/              Internal SMTP email relay (no public port). All email sending goes here.
  email-api-models/       Refit client + SendEmailRequest + EmailApiSettings (shared by api and cv-search-job).
  cv-matcher-api/         Internal CV match engine (port 8082). Runs CvMatcher + CvSearch DB migrations.
  cv-matcher-api-models/  DTOs shared between api and cv-matcher-api (incl. JobSearchSettings).
  rag-api/                Internal RAG/vector-search service (port 8081).
  rag-api-models/         DTOs shared with rag-api.
  common/                 Cross-service infrastructure primitives (DatabaseSettings, InternalApiSettings, etc.).
  shared-data/            Abstract BaseEntity base class. No DbContext.
  cv-matcher-data/        CvMatcherDbContext + entities + migrations (schema: cvMatcher). Owns AiPrompts table.
  cv-search-data/         CvSearchDbContext + entities + migrations (schema: cvSearch).
  email-api-data/         EmailApiDbContext + entities + migrations (schema: emailApi). Owns EmailTemplates table.
  rag-data/               RagDbContext + entities + migrations (schema: rag).
  myai-data/              MyAiDbContext + entities + migrations (schema: myAi). Keeps only html.* templates.
Helpers/
  startup-helpers/        Shared Program.cs bootstrap: Serilog, Swagger, .env loading, Azure Key Vault, middleware.
  common-helpers/         Utility helpers.
Jobs/
  job-scheduler/          IJobTask + JobSchedulerHostedService — the reusable scheduled-job engine.
  cv-cleanup-job/         Worker: deletes old CVs from file storage. Runs hourly.
  cv-cleanup-job-models/  Job-specific models for cv-cleanup-job (proactive; currently empty).
  cv-search-job/          Worker: picks up pending job search sessions, scrapes providers, emails results.
  cv-search-job-models/   Job-specific models for cv-search-job (proactive; currently empty).
web/                      Razor Pages / Blazor front-end (port 5140).
docker-compose/           docker-compose.yml + .env file.
```

Virtual solution folders in `.sln`: `Apis` (executables + web), `Models` (DTOs/contracts), `Data` (data layers), `Jobs`, `Helpers`.

## Build & restore

```powershell
dotnet restore myAi.sln
dotnet build myAi.sln
```

## Running locally with Docker

```powershell
docker compose -f docker-compose/docker-compose.yml up --build
```

Config lives in `docker-compose/.env`. All env vars use `${VAR:-default}` fallback syntax.

## Database schemas

| Schema      | Owner DbContext       | Migrations project    | Startup project       |
|-------------|----------------------|-----------------------|-----------------------|
| `cvMatcher` | `CvMatcherDbContext` | `cv-matcher-data`     | `cv-matcher-api`      |
| `emailApi`  | `EmailApiDbContext`  | `email-api-data`      | `email-api`           |
| `rag`       | `RagDbContext`       | `rag-data`            | `rag-api`             |
| `cvSearch`  | `CvSearchDbContext`  | `cv-search-data`      | `cv-matcher-api`      |
| `myAi`      | `MyAiDbContext`      | `myai-data`           | `api`                 |

Both `cv-matcher-api` and `cv-search-job` register `CvSearchDbContext` and call `db.Database.Migrate()` on startup (idempotent — safe for both to run).

`api` and `cv-search-job` also register `EmailApiDbContext` (read-only — `email-api` is the sole migration owner). They use it to load email templates via `IEmailTemplateService` (10-min cache, singleton).

## EF Core migrations

```powershell
# cv-matcher-data (schema: cvMatcher)
dotnet ef migrations add <MigrationName> `
  --context CvMatcherDbContext `
  --project Apis/cv-matcher-data `
  --startup-project Apis/cv-matcher-api

# email-api-data (schema: emailApi)
dotnet ef migrations add <MigrationName> `
  --context EmailApiDbContext `
  --project Apis/email-api-data `
  --startup-project Apis/email-api

# rag-data (schema: rag)
dotnet ef migrations add <MigrationName> `
  --context RagDbContext `
  --project Apis/rag-data `
  --startup-project Apis/rag-api

# cv-search-data (schema: cvSearch)
dotnet ef migrations add <MigrationName> `
  --context CvSearchDbContext `
  --project Apis/cv-search-data `
  --startup-project Apis/cv-matcher-api

# myai-data (schema: myAi)
dotnet ef migrations add <MigrationName> `
  --context MyAiDbContext `
  --project Apis/myai-data `
  --startup-project Apis/api
```

EF tools version warning ("older than runtime") is expected and harmless. The `HostAbortedException` output during migration scaffolding is normal — EF starts the host to discover DbContext then aborts it.

## Service dependency chain

```
web → api → cv-matcher-api → rag-api
  ↓     ↓
  |   email-api
  ↓     ↑
cv-search-job
```

`api` and `cv-search-job` both call `email-api` for all outbound email (SMTP).  
`api` never talks directly to `rag-api` — always via `cv-matcher-api`.

## Internal API key auth

All internal service-to-service calls require the `X-Internal-Api-Key` header.

| Caller | Target | Env var for key |
|--------|--------|-----------------|
| `api`, `cv-search-job` | `email-api` | `EmailApi__InternalApiKey` |
| `api`, `cv-search-job` | `cv-matcher-api` | `CvMatcherApi__InternalApiKey` |
| `cv-matcher-api` | `rag-api` | `RagApi__InternalApiKey` |

`startup-helpers` provides `UseInternalApiKeyProtection()` middleware (reads `InternalApi:ApiKey`); enforced on `cv-matcher-api`, `rag-api`, and `email-api`.

## Shared file storage

CV PDFs are written by `api` to `Apis/api/Files/` and read by `cv-cleanup-job`, `cv-search-job`, and `email-api` (for email attachments).  
All four containers mount the same bind volume:
```yaml
- ${FILES_PATH:-/opt/myai/files}:/app/Files
```
The path inside containers is controlled by `FileStorage__Path` (default: `Files`).  
`email-api` receives only the relative filename (e.g. `abc123.pdf`) and resolves it against `FileStorage__Path`.

## Job task pattern

Every background worker uses the same pattern from `job-scheduler`:
1. Implement `IJobTask` (has `TaskType` string + `ExecuteAsync(CancellationToken)`)
2. Register as singleton: `services.AddSingleton<IEnumerable<IJobTask>>(sp => new IJobTask[] { ... })`
3. Register `JobSchedulerHostedService` as hosted service
4. Configure in appsettings under `Jobs:Tasks` array: `TaskType`, `Enabled`, `Interval`

## Program.cs conventions

Every service follows this structure:
1. `StartupExtensions.LoadDotEnvFile()` — must be first, loads `docker-compose/.env`
2. `StartupExtensions.GetApplicationVersion(Assembly.GetExecutingAssembly())`
3. `builder.ConfigureJsonSerilog(ServiceName, appVersion)` from startup-helpers
4. `builder.AddAzureKeyVaultIfConfigured()` (APIs only)
5. `app.UseDefaultSerilogRequestLogging()`
6. `app.UseJsonExceptionHandler(ServiceName)`
7. EF migrations in a scoped block before `app.Run()`

## Coding conventions

- XML doc comments (`/// <summary>`) on all public methods, interfaces, and non-trivial private/protected helpers; Swagger annotations on public controller actions
- Inline `//` comments for non-obvious logic; avoid restating what the code already says clearly
- Use `$$"""..."""` raw string literals (not `$"""`) when the content contains CSS or other curly-brace-heavy text — avoids CS9006 brace-escaping errors
- `sealed` on all concrete service classes
- Settings classes injected via `IOptions<T>` — registered with `Configure<T>(config.GetSection("..."))`
- Refit clients configured via a shared local function when multiple clients share the same base URL and auth header (see `api/Program.cs` → `ConfigureCvMatcherApiClient`)
