Files
myAi/CLAUDE.md
T
claude e95ed36647 refactor: restructure solution into -models/-data/-api project taxonomy
Phases 1-10 of the planned refactoring:

Phase 1: rename shared-models -> common
  - namespace Shared.Models -> Common throughout
  - remove stale AspNetCore.Http.Features 5.0 reference

Phase 2: create shared-data with abstract BaseEntity
  - BaseEntity: required string Id { get; init; } + DateTime CreatedAt { get; init; }

Phase 3: rename myai-models -> myai-data
  - namespace MyAi.Models -> MyAi.Data
  - MigrationsAssembly("myai-data")

Phase 4: rename cv-search-models -> cv-search-data
  - namespace CvSearch.Models -> CvSearch.Data
  - move JobSearchSettings to cv-matcher-api-models
  - JobSearch*Entity now inherits BaseEntity

Phase 5: extract rag-data from rag-api
  - new project: Apis/rag-data with RagDbContext + entities + migrations
  - RagDocumentEntity inherits BaseEntity; cache entities use CacheKey PK
  - fix duplicate AddHttpClient<RagAiClient>/AddScoped registrations in rag-api
  - MigrationsAssembly("rag-data")

Phase 6: extract cv-matcher-data from cv-matcher-api
  - new project: Apis/cv-matcher-data with CvMatcherDbContext + entities + migrations
  - CvMatchResultEntity inherits BaseEntity; CvMatcherChatCacheEntity uses CacheKey PK
  - MigrationsAssembly("cv-matcher-data")

Phase 7: create empty cv-cleanup-job-models and cv-search-job-models

Phase 8: update all 5 Dockerfiles for renamed/new projects

Phase 9: reorganise .sln virtual folders (Apis/Jobs/Models/Data/Helpers)
  - update root CLAUDE.md with new project taxonomy and migration commands
  - update cv-matcher-api/CLAUDE.md and cv-search-job/CLAUDE.md

Phase 10: add Directory.Packages.props for centralised NuGet versions
  - remove Version= from all PackageReference elements in active .csproj files

No database changes. No runtime behaviour changes.
All MigrationId strings in __EFMigrationsHistory are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:26:03 +03:00

199 lines
9.1 KiB
Markdown

# 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
## 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.
cv-matcher-api/ Internal CV match engine (port 8082). Runs CvMatcher + CvSearch + MyAi 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).
cv-search-data/ CvSearchDbContext + entities + migrations (schema: cvSearch).
rag-data/ RagDbContext + entities + migrations (schema: rag).
myai-data/ MyAiDbContext + entities + migrations (schema: myAi).
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` |
| `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).
## 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
# 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
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<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
- 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<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`)