# 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 (JSON structured logging, Console + File + Email sinks) - MailKit for SMTP - Docker Compose for local and production deployment - Watchtower for automatic container updates in production ## Solution layout ``` Apis/ api/ Public-facing proxy API (port 8080). Handles CORS, rate limiting, captcha, email. api-models/ DTOs and settings shared by api only. cv-matcher-api/ Internal CV match engine (port 8082). Owns cvMatcher + cvSearch DB schemas. cv-matcher-api-models/ DTOs shared between api and cv-matcher-api. cv-search-models/ EF entities + DbContext for cvSearch schema. Shared by cv-matcher-api and cv-search-job. rag-api/ Internal RAG/vector-search service (port 8081). Owns rag DB schema. rag-api-models/ DTOs shared with rag-api. shared-models/ Cross-service shared models (DatabaseSettings, etc.). 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-search-job/ Worker: picks up pending job search sessions, scrapes providers, emails results. web/ Razor Pages / Blazor front-end (port 5000). docker-compose/ docker-compose.yml + .env file. ``` ## 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 assembly | |-------------|----------------------|-----------------------| | `cvMatcher` | `CvMatcherDbContext` | `cv-matcher-api` | | `rag` | `RagDbContext` | `rag-api` | | `cvSearch` | `CvSearchDbContext` | `cv-search-models` | Both `cv-matcher-api` and `cv-search-job` register `CvSearchDbContext` and call `db.Database.Migrate()` on startup (idempotent — safe for both to run). ## EF Core migrations ```powershell # Add a migration to cv-search-models dotnet ef migrations add \ --context CvSearchDbContext \ --project Apis/cv-search-models \ --startup-project Apis/cv-matcher-api # Add a migration to cv-matcher-api dotnet ef migrations add \ --context CvMatcherDbContext \ --project Apis/cv-matcher-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 ↑ cv-search-job ``` `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. The key is shared via the `CvMatcherApi__InternalApiKey` and `RagApi__InternalApiKey` env vars. `startup-helpers` provides `UseInternalApiKeyProtection()` middleware that enforces it on `cv-matcher-api` and `rag-api`. ## Shared file storage CV PDFs are written by `api` to `Apis/api/Files/` and read by `cv-cleanup-job` and `cv-search-job`. All three containers mount the same bind volume: ```yaml - ../Apis/api/Files:/app/Files ``` The path inside containers is controlled by `FileStorage__Path` (default: `Files`). ## 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>(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 - No XML doc comments on internal code; Swagger annotations on public controller actions - No explanatory inline comments — code should be self-describing - 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` — registered with `Configure(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`)