Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
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:
- Add the plan as a Gitea Wiki page in the relevant repository (under a
Features/orPlans/namespace) - Create Gitea Issues — one per logical work chunk — and link them to the Wiki page
- Reference the issue number in commit messages (
Closes #N) - 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, enrichedApplication/Environment/AppVersion. The host's Grafana Alloy agent ships stdout → Loki; view/query in Grafana. No file sink; optional email sink only ifSerilogEmail:*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
dotnet restore myAi.sln
dotnet build myAi.sln
Running locally with Docker
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
# 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:
- ${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:
- Implement
IJobTask(hasTaskTypestring +ExecuteAsync(CancellationToken)) - Register as singleton:
services.AddSingleton<IEnumerable<IJobTask>>(sp => new IJobTask[] { ... }) - Register
JobSchedulerHostedServiceas hosted service - Configure in appsettings under
Jobs:Tasksarray:TaskType,Enabled,Interval
Program.cs conventions
Every service follows this structure:
StartupExtensions.LoadDotEnvFile()— must be first, loadsdocker-compose/.envStartupExtensions.GetApplicationVersion(Assembly.GetExecutingAssembly())builder.ConfigureJsonSerilog(ServiceName, appVersion)from startup-helpersbuilder.AddAzureKeyVaultIfConfigured()(APIs only)app.UseDefaultSerilogRequestLogging()app.UseJsonExceptionHandler(ServiceName)- 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 sealedon all concrete service classes- Settings classes injected via
IOptions<T>— registered withConfigure<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)