diff --git a/Apis/api-models/Requests/UploadCvRequest.cs b/Apis/api-models/Requests/UploadCvRequest.cs index 18bd5db..31349d0 100644 --- a/Apis/api-models/Requests/UploadCvRequest.cs +++ b/Apis/api-models/Requests/UploadCvRequest.cs @@ -1,4 +1,4 @@ -using Shared.Models.Requests; +using Common.Requests; namespace Models.Requests { diff --git a/Apis/api-models/api-models.csproj b/Apis/api-models/api-models.csproj index f9c95a3..5e0613b 100644 --- a/Apis/api-models/api-models.csproj +++ b/Apis/api-models/api-models.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -8,11 +8,11 @@ - + - + diff --git a/Apis/api/Controllers/CaptchaController.cs b/Apis/api/Controllers/CaptchaController.cs index 53a715d..b050523 100644 --- a/Apis/api/Controllers/CaptchaController.cs +++ b/Apis/api/Controllers/CaptchaController.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Options; using Models.Settings; using Swashbuckle.AspNetCore.Annotations; using Models.Requests; -using Shared.Models.Responses; +using Common.Responses; namespace Api.Controllers { diff --git a/Apis/api/Controllers/ContactController.cs b/Apis/api/Controllers/ContactController.cs index 920b594..b95e2ea 100644 --- a/Apis/api/Controllers/ContactController.cs +++ b/Apis/api/Controllers/ContactController.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; using Models.Settings; using Models.Requests; using Swashbuckle.AspNetCore.Annotations; -using Shared.Models.Responses; +using Common.Responses; namespace Api.Controllers { diff --git a/Apis/api/Controllers/CvMatcherController.cs b/Apis/api/Controllers/CvMatcherController.cs index 76b07a9..fe734a8 100644 --- a/Apis/api/Controllers/CvMatcherController.cs +++ b/Apis/api/Controllers/CvMatcherController.cs @@ -9,8 +9,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.Annotations; -using Shared.Models.Responses; -using MyAi.Models.Services; +using Common.Responses; +using MyAi.Data.Services; namespace Api.Controllers; diff --git a/Apis/api/Controllers/FileDownloadController.cs b/Apis/api/Controllers/FileDownloadController.cs index 58765de..f12a5fb 100644 --- a/Apis/api/Controllers/FileDownloadController.cs +++ b/Apis/api/Controllers/FileDownloadController.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Swashbuckle.AspNetCore.Annotations; -using Shared.Models.Responses; +using Common.Responses; namespace Api.Controllers { diff --git a/Apis/api/Dockerfile b/Apis/api/Dockerfile index d344070..e3d147b 100644 --- a/Apis/api/Dockerfile +++ b/Apis/api/Dockerfile @@ -3,19 +3,21 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY Apis/api/api.csproj Apis/api/ -COPY Apis/shared-models/shared-models.csproj Apis/shared-models/ +COPY Apis/common/common.csproj Apis/common/ COPY Apis/api-models/api-models.csproj Apis/api-models/ COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/ -COPY Apis/myai-models/myai-models.csproj Apis/myai-models/ +COPY Apis/myai-data/myai-data.csproj Apis/myai-data/ +COPY Apis/shared-data/shared-data.csproj Apis/shared-data/ COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/ RUN dotnet restore Apis/api/api.csproj COPY Apis/api/ Apis/api/ -COPY Apis/shared-models/ Apis/shared-models/ +COPY Apis/common/ Apis/common/ COPY Apis/api-models/ Apis/api-models/ COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/ -COPY Apis/myai-models/ Apis/myai-models/ +COPY Apis/myai-data/ Apis/myai-data/ +COPY Apis/shared-data/ Apis/shared-data/ COPY Helpers/startup-helpers/ Helpers/startup-helpers/ RUN dotnet publish Apis/api/api.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false @@ -27,4 +29,4 @@ ENV ASPNETCORE_URLS=http://0.0.0.0:8080 COPY --from=build /app/publish . -ENTRYPOINT ["dotnet", "api.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "api.dll"] diff --git a/Apis/api/Program.cs b/Apis/api/Program.cs index 4b92fbb..ae73a22 100644 --- a/Apis/api/Program.cs +++ b/Apis/api/Program.cs @@ -3,11 +3,11 @@ using Api.Services; using Api.Services.Contracts; using Microsoft.EntityFrameworkCore; using Models.Settings; -using MyAi.Models.Data; -using MyAi.Models.Services; +using MyAi.Data; +using MyAi.Data.Services; using Refit; using Serilog; -using Shared.Models.Settings; +using Common.Settings; using StartupHelpers; StartupExtensions.LoadDotEnvFile(); @@ -39,7 +39,7 @@ try var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration); options.UseSqlServer(connectionString, sql => { - sql.MigrationsAssembly("myai-models"); + sql.MigrationsAssembly("myai-data"); sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName); }); }); diff --git a/Apis/api/Services/SmtpEmailSender.cs b/Apis/api/Services/SmtpEmailSender.cs index d17ffe7..ee564b4 100644 --- a/Apis/api/Services/SmtpEmailSender.cs +++ b/Apis/api/Services/SmtpEmailSender.cs @@ -6,7 +6,7 @@ using MimeKit; using Models.Settings; using Models.Requests; using CvMatcher.Models.Responses; -using MyAi.Models.Services; +using MyAi.Data.Services; namespace Api.Services { diff --git a/Apis/api/api.csproj b/Apis/api/api.csproj index d369786..4a628cc 100644 --- a/Apis/api/api.csproj +++ b/Apis/api/api.csproj @@ -16,18 +16,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -37,9 +37,9 @@ - + - + diff --git a/Apis/shared-models/Requests/UploadFileRequest.cs b/Apis/common/Requests/UploadFileRequest.cs similarity index 86% rename from Apis/shared-models/Requests/UploadFileRequest.cs rename to Apis/common/Requests/UploadFileRequest.cs index c9ed0b2..99a24dc 100644 --- a/Apis/shared-models/Requests/UploadFileRequest.cs +++ b/Apis/common/Requests/UploadFileRequest.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; using System.ComponentModel.DataAnnotations; -namespace Shared.Models.Requests +namespace Common.Requests { public class UploadFileRequest { diff --git a/Apis/shared-models/Responses/ErrorResponse.cs b/Apis/common/Responses/ErrorResponse.cs similarity index 85% rename from Apis/shared-models/Responses/ErrorResponse.cs rename to Apis/common/Responses/ErrorResponse.cs index c688715..a1262d8 100644 --- a/Apis/shared-models/Responses/ErrorResponse.cs +++ b/Apis/common/Responses/ErrorResponse.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Responses; +namespace Common.Responses; public sealed class ErrorResponse { diff --git a/Apis/shared-models/Settings/AiSettings.cs b/Apis/common/Settings/AiSettings.cs similarity index 73% rename from Apis/shared-models/Settings/AiSettings.cs rename to Apis/common/Settings/AiSettings.cs index 56ff34c..f6fef72 100644 --- a/Apis/shared-models/Settings/AiSettings.cs +++ b/Apis/common/Settings/AiSettings.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Settings +namespace Common.Settings { public class AiSettings { diff --git a/Apis/shared-models/Settings/DatabaseSettings.cs b/Apis/common/Settings/DatabaseSettings.cs similarity index 90% rename from Apis/shared-models/Settings/DatabaseSettings.cs rename to Apis/common/Settings/DatabaseSettings.cs index d5f89e9..4a3961a 100644 --- a/Apis/shared-models/Settings/DatabaseSettings.cs +++ b/Apis/common/Settings/DatabaseSettings.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Settings +namespace Common.Settings { public class DatabaseSettings { diff --git a/Apis/shared-models/Settings/InternalApiSettings.cs b/Apis/common/Settings/InternalApiSettings.cs similarity index 82% rename from Apis/shared-models/Settings/InternalApiSettings.cs rename to Apis/common/Settings/InternalApiSettings.cs index 14b0637..d988232 100644 --- a/Apis/shared-models/Settings/InternalApiSettings.cs +++ b/Apis/common/Settings/InternalApiSettings.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Settings +namespace Common.Settings { public class InternalApiSettings { diff --git a/Apis/shared-models/Settings/OllamaSettings.cs b/Apis/common/Settings/OllamaSettings.cs similarity index 86% rename from Apis/shared-models/Settings/OllamaSettings.cs rename to Apis/common/Settings/OllamaSettings.cs index 6cb4584..2b3a11f 100644 --- a/Apis/shared-models/Settings/OllamaSettings.cs +++ b/Apis/common/Settings/OllamaSettings.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Settings +namespace Common.Settings { public class OllamaSettings { diff --git a/Apis/shared-models/Settings/OpenAiSettings.cs b/Apis/common/Settings/OpenAiSettings.cs similarity index 86% rename from Apis/shared-models/Settings/OpenAiSettings.cs rename to Apis/common/Settings/OpenAiSettings.cs index 603a55f..e280784 100644 --- a/Apis/shared-models/Settings/OpenAiSettings.cs +++ b/Apis/common/Settings/OpenAiSettings.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Settings +namespace Common.Settings { public class OpenAiSettings { diff --git a/Apis/shared-models/Settings/RateLimitingSettings.cs b/Apis/common/Settings/RateLimitingSettings.cs similarity index 94% rename from Apis/shared-models/Settings/RateLimitingSettings.cs rename to Apis/common/Settings/RateLimitingSettings.cs index 2f1a730..38bb837 100644 --- a/Apis/shared-models/Settings/RateLimitingSettings.cs +++ b/Apis/common/Settings/RateLimitingSettings.cs @@ -1,4 +1,4 @@ -namespace Shared.Models.Settings +namespace Common.Settings { public class RateLimitingSettings { diff --git a/Apis/shared-models/shared-models.csproj b/Apis/common/common.csproj similarity index 53% rename from Apis/shared-models/shared-models.csproj rename to Apis/common/common.csproj index 0cea718..3762a2e 100644 --- a/Apis/shared-models/shared-models.csproj +++ b/Apis/common/common.csproj @@ -1,14 +1,15 @@ - + net10.0 - Shared.Models + common + Common enable enable - + diff --git a/Apis/cv-matcher-api-models/Settings/AiSettings.cs b/Apis/cv-matcher-api-models/Settings/AiSettings.cs index 839ddb8..0f1e7d5 100644 --- a/Apis/cv-matcher-api-models/Settings/AiSettings.cs +++ b/Apis/cv-matcher-api-models/Settings/AiSettings.cs @@ -1,8 +1,8 @@ -using Shared.Models.Settings; +using Common.Settings; namespace CvMatcher.Models.Settings; -public sealed class AiSettings : Shared.Models.Settings.AiSettings +public sealed class AiSettings : Common.Settings.AiSettings { public OpenAiSettings OpenAI { get; set; } = new(); public OllamaSettings Ollama { get; set; } = new(); diff --git a/Apis/cv-matcher-api-models/Settings/JobSearchSettings.cs b/Apis/cv-matcher-api-models/Settings/JobSearchSettings.cs new file mode 100644 index 0000000..5afaa9d --- /dev/null +++ b/Apis/cv-matcher-api-models/Settings/JobSearchSettings.cs @@ -0,0 +1,21 @@ +namespace CvMatcher.Models.Settings; + +public sealed class JobSearchSettings +{ + public bool Enabled { get; set; } = true; + public string JobSearchLinkBaseUrl { get; set; } = string.Empty; + public int TokenExpiryDays { get; set; } = 7; + public int MinMatchScore { get; set; } = 15; + public int MaxJobsToMatch { get; set; } = 15; + public List Providers { get; set; } = []; +} + +public sealed class JobProviderConfig +{ + public string Name { get; set; } = string.Empty; + public bool Enabled { get; set; } = true; + public string SearchUrlTemplate { get; set; } = string.Empty; + public string JobLinkContains { get; set; } = string.Empty; + public List InitialKeywords { get; set; } = []; + public int MaxResults { get; set; } = 20; +} diff --git a/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj b/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj index aeeb632..9dddb25 100644 --- a/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj +++ b/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj @@ -8,7 +8,7 @@ - + diff --git a/Apis/cv-matcher-api/CLAUDE.md b/Apis/cv-matcher-api/CLAUDE.md index a310eb4..4867458 100644 --- a/Apis/cv-matcher-api/CLAUDE.md +++ b/Apis/cv-matcher-api/CLAUDE.md @@ -36,8 +36,8 @@ Default model: `gpt-4o-mini`. Timeout: 90 s. Both contexts use the same SQL Server connection string (from `Database:*` settings). -- `CvMatcherDbContext` — schema `cvMatcher`; migrations in `cv-matcher-api` assembly -- `CvSearchDbContext` — schema `cvSearch`; migrations in `cv-search-models` assembly (MigrationsAssembly = "cv-search-models") +- `CvMatcherDbContext` — schema `cvMatcher`; migrations in `cv-matcher-data` assembly (`Apis/cv-matcher-data/`) +- `CvSearchDbContext` — schema `cvSearch`; migrations in `cv-search-data` assembly (`Apis/cv-search-data/`) ## Keyword extraction (JobTokenService.ExtractKeywords) diff --git a/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs b/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs index 05014e6..8fd03e6 100644 --- a/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs +++ b/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Options; using CvMatcher.Models.Settings; -using Api.Data.Repositories.Contracts; +using CvMatcher.Data.Repositories.Contracts; using Api.Clients.Ai.Contracts; using CommonHelpers; diff --git a/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs b/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs index ae8f9cb..3fe3968 100644 --- a/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs +++ b/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs @@ -3,7 +3,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Api.Clients.Ai.Contracts; -using Api.Data.Repositories.Contracts; +using CvMatcher.Data.Repositories.Contracts; using CommonHelpers; using CvMatcher.Models.Settings; using Microsoft.Extensions.Options; diff --git a/Apis/cv-matcher-api/Controllers/CvController.cs b/Apis/cv-matcher-api/Controllers/CvController.cs index 7e54f97..c0d0ee4 100644 --- a/Apis/cv-matcher-api/Controllers/CvController.cs +++ b/Apis/cv-matcher-api/Controllers/CvController.cs @@ -2,9 +2,9 @@ using CvMatcher.Models.Requests; using Api.Services.Contracts; using Microsoft.AspNetCore.Mvc; using CvMatcher.Models.Responses; -using Shared.Models.Requests; +using Common.Requests; using Swashbuckle.AspNetCore.Annotations; -using Shared.Models.Responses; +using Common.Responses; namespace Api.Controllers; diff --git a/Apis/cv-matcher-api/Controllers/JobSearchController.cs b/Apis/cv-matcher-api/Controllers/JobSearchController.cs index 95d832c..65bd1eb 100644 --- a/Apis/cv-matcher-api/Controllers/JobSearchController.cs +++ b/Apis/cv-matcher-api/Controllers/JobSearchController.cs @@ -2,7 +2,7 @@ using Api.Services.Contracts; using CvMatcher.Models.Requests; using CvMatcher.Models.Responses; using Microsoft.AspNetCore.Mvc; -using Shared.Models.Responses; +using Common.Responses; using Swashbuckle.AspNetCore.Annotations; namespace Api.Controllers; diff --git a/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs b/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs index e128a69..6241862 100644 --- a/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs +++ b/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs @@ -1,6 +1,6 @@ using CvMatcher.Models.Responses; -namespace Api.Data.Repositories.Contracts; +namespace CvMatcher.Data.Repositories.Contracts; public interface IMatcherRepository { diff --git a/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs b/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs index 5ed9b0b..85ada91 100644 --- a/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs +++ b/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs @@ -1,11 +1,11 @@ using System.Text.Json; -using Api.Data; -using Api.Data.Entities; -using Api.Data.Repositories.Contracts; +using CvMatcher.Data; +using CvMatcher.Data.Entities; +using CvMatcher.Data.Repositories.Contracts; using CvMatcher.Models.Responses; using Microsoft.EntityFrameworkCore; -namespace Api.Data.Repositories; +namespace CvMatcher.Data.Repositories; public sealed class EfMatcherRepository : IMatcherRepository { diff --git a/Apis/cv-matcher-api/Dockerfile b/Apis/cv-matcher-api/Dockerfile index 5803a8e..aa636ce 100644 --- a/Apis/cv-matcher-api/Dockerfile +++ b/Apis/cv-matcher-api/Dockerfile @@ -3,20 +3,24 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY Apis/cv-matcher-api/cv-matcher-api.csproj Apis/cv-matcher-api/ -COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/ -COPY Apis/shared-models/shared-models.csproj Apis/shared-models/ +COPY Apis/cv-search-data/cv-search-data.csproj Apis/cv-search-data/ +COPY Apis/cv-matcher-data/cv-matcher-data.csproj Apis/cv-matcher-data/ +COPY Apis/common/common.csproj Apis/common/ COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/ -COPY Apis/myai-models/myai-models.csproj Apis/myai-models/ +COPY Apis/myai-data/myai-data.csproj Apis/myai-data/ +COPY Apis/shared-data/shared-data.csproj Apis/shared-data/ COPY Helpers/common-helpers/common-helpers.csproj Helpers/common-helpers/ COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/ RUN dotnet restore Apis/cv-matcher-api/cv-matcher-api.csproj COPY Apis/cv-matcher-api/ Apis/cv-matcher-api/ -COPY Apis/cv-search-models/ Apis/cv-search-models/ -COPY Apis/shared-models/ Apis/shared-models/ +COPY Apis/cv-search-data/ Apis/cv-search-data/ +COPY Apis/cv-matcher-data/ Apis/cv-matcher-data/ +COPY Apis/common/ Apis/common/ COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/ -COPY Apis/myai-models/ Apis/myai-models/ +COPY Apis/myai-data/ Apis/myai-data/ +COPY Apis/shared-data/ Apis/shared-data/ COPY Helpers/common-helpers/ Helpers/common-helpers/ COPY Helpers/startup-helpers/ Helpers/startup-helpers/ @@ -29,4 +33,4 @@ ENV ASPNETCORE_URLS=http://0.0.0.0:8080 COPY --from=build /app/publish . -ENTRYPOINT ["dotnet", "cv-matcher-api.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "cv-matcher-api.dll"] diff --git a/Apis/cv-matcher-api/Program.cs b/Apis/cv-matcher-api/Program.cs index c0b0f94..0913aec 100644 --- a/Apis/cv-matcher-api/Program.cs +++ b/Apis/cv-matcher-api/Program.cs @@ -2,20 +2,19 @@ using Api.Clients.Ai; using Api.Clients.Ai.Contracts; using Api.Clients.Api; using Api.Clients.Api.Contracts; -using Api.Data; -using Api.Data.Repositories; -using Api.Data.Repositories.Contracts; +using CvMatcher.Data; +using CvMatcher.Data.Repositories; +using CvMatcher.Data.Repositories.Contracts; using Api.Services; using Api.Services.Contracts; using CvMatcher.Models.Settings; -using CvSearch.Models.Data; -using CvSearch.Models.Settings; +using CvSearch.Data; using Microsoft.EntityFrameworkCore; -using MyAi.Models.Data; -using MyAi.Models.Services; +using MyAi.Data; +using MyAi.Data.Services; using Refit; using Serilog; -using Shared.Models.Settings; +using Common.Settings; using StartupHelpers; using System.Reflection; @@ -63,6 +62,7 @@ try options.UseSqlServer(connectionString, sql => { sql.MigrationsHistoryTable(CvMatcherDbContext.MigrationTableName, CvMatcherDbContext.SchemaName); + sql.MigrationsAssembly("cv-matcher-data"); }); }); @@ -71,7 +71,7 @@ try var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration); options.UseSqlServer(connectionString, sql => { - sql.MigrationsAssembly("cv-search-models"); + sql.MigrationsAssembly("cv-search-data"); sql.MigrationsHistoryTable(CvSearchDbContext.MigrationTableName, CvSearchDbContext.SchemaName); }); }); @@ -81,7 +81,7 @@ try var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration); options.UseSqlServer(connectionString, sql => { - sql.MigrationsAssembly("myai-models"); + sql.MigrationsAssembly("myai-data"); sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName); }); }); diff --git a/Apis/cv-matcher-api/Services/CvMatcherService.cs b/Apis/cv-matcher-api/Services/CvMatcherService.cs index 5d34651..5f699fd 100644 --- a/Apis/cv-matcher-api/Services/CvMatcherService.cs +++ b/Apis/cv-matcher-api/Services/CvMatcherService.cs @@ -1,13 +1,13 @@ using System.Text.Json; using Api.Clients.Ai.Contracts; using Api.Clients.Api.Contracts; -using Api.Data.Repositories.Contracts; +using CvMatcher.Data.Repositories.Contracts; using CvMatcher.Models.Requests; using CvMatcher.Models.Responses; using CvMatcher.Models.Settings; using Api.Services.Contracts; using Microsoft.Extensions.Options; -using MyAi.Models.Services; +using MyAi.Data.Services; namespace Api.Services; diff --git a/Apis/cv-matcher-api/Services/JobTokenService.cs b/Apis/cv-matcher-api/Services/JobTokenService.cs index 4640438..8b1f2d8 100644 --- a/Apis/cv-matcher-api/Services/JobTokenService.cs +++ b/Apis/cv-matcher-api/Services/JobTokenService.cs @@ -3,9 +3,9 @@ using System.Text.RegularExpressions; using Api.Clients.Api.Contracts; using Api.Services.Contracts; using CvMatcher.Models.Responses; -using CvSearch.Models.Data; -using CvSearch.Models.Data.Entities; -using CvSearch.Models.Settings; +using CvSearch.Data; +using CvSearch.Data.Entities; +using CvMatcher.Models.Settings; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; diff --git a/Apis/cv-matcher-api/cv-matcher-api.csproj b/Apis/cv-matcher-api/cv-matcher-api.csproj index 5bed5f5..561c0ea 100644 --- a/Apis/cv-matcher-api/cv-matcher-api.csproj +++ b/Apis/cv-matcher-api/cv-matcher-api.csproj @@ -58,30 +58,31 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + + - - - - - + + + + + + diff --git a/Apis/cv-matcher-api/Data/CvMatcherDbContext.cs b/Apis/cv-matcher-data/CvMatcherDbContext.cs similarity index 96% rename from Apis/cv-matcher-api/Data/CvMatcherDbContext.cs rename to Apis/cv-matcher-data/CvMatcherDbContext.cs index 5889116..52e12ce 100644 --- a/Apis/cv-matcher-api/Data/CvMatcherDbContext.cs +++ b/Apis/cv-matcher-data/CvMatcherDbContext.cs @@ -1,8 +1,7 @@ -using Api.Data.Entities; +using CvMatcher.Data.Entities; using Microsoft.EntityFrameworkCore; -namespace Api.Data; - +namespace CvMatcher.Data; public sealed class CvMatcherDbContext : DbContext { diff --git a/Apis/cv-matcher-api/Data/Entities/CvMatchResultEntity.cs b/Apis/cv-matcher-data/Entities/CvMatchResultEntity.cs similarity index 59% rename from Apis/cv-matcher-api/Data/Entities/CvMatchResultEntity.cs rename to Apis/cv-matcher-data/Entities/CvMatchResultEntity.cs index d86a1b0..ac0a9c5 100644 --- a/Apis/cv-matcher-api/Data/Entities/CvMatchResultEntity.cs +++ b/Apis/cv-matcher-data/Entities/CvMatchResultEntity.cs @@ -1,12 +1,12 @@ -namespace Api.Data.Entities; +using Shared.Data.Entities; -public sealed class CvMatchResultEntity +namespace CvMatcher.Data.Entities; + +public sealed class CvMatchResultEntity : BaseEntity { - public string Id { get; set; } = string.Empty; public string CvDocumentId { get; set; } = string.Empty; public string JobDocumentId { get; set; } = string.Empty; public string Language { get; set; } = "en"; public string ResultJson { get; set; } = string.Empty; public int Score { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/Apis/cv-matcher-api/Data/Entities/CvMatcherChatCacheEntity.cs b/Apis/cv-matcher-data/Entities/CvMatcherChatCacheEntity.cs similarity index 80% rename from Apis/cv-matcher-api/Data/Entities/CvMatcherChatCacheEntity.cs rename to Apis/cv-matcher-data/Entities/CvMatcherChatCacheEntity.cs index 4ad845a..2bb669f 100644 --- a/Apis/cv-matcher-api/Data/Entities/CvMatcherChatCacheEntity.cs +++ b/Apis/cv-matcher-data/Entities/CvMatcherChatCacheEntity.cs @@ -1,5 +1,6 @@ -namespace Api.Data.Entities; +namespace CvMatcher.Data.Entities; +// CacheKey PK — BaseEntity not applicable public sealed class CvMatcherChatCacheEntity { public string CacheKey { get; set; } = string.Empty; diff --git a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs similarity index 92% rename from Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs rename to Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs index 1ef6774..42ef9a7 100644 --- a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs +++ b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs @@ -1,6 +1,6 @@ -// +// using System; -using Api.Data; +using CvMatcher.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Api.Migrations +namespace CvMatcher.Data.Migrations { [DbContext(typeof(CvMatcherDbContext))] [Migration("20260507140442_InitialCvMatcherSchema")] @@ -26,7 +26,7 @@ namespace Api.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Api.Data.Entities.CvMatchResultEntity", b => + modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -62,7 +62,7 @@ namespace Api.Migrations b.ToTable("Results", "cvMatcher"); }); - modelBuilder.Entity("Api.Data.Entities.CvMatcherChatCacheEntity", b => + modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) diff --git a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.cs b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.cs similarity index 98% rename from Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.cs rename to Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.cs index 4c62b0f..9e9c62f 100644 --- a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.cs +++ b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.cs @@ -1,9 +1,9 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Api.Migrations +namespace CvMatcher.Data.Migrations { /// public partial class InitialCvMatcherSchema : Migration diff --git a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs similarity index 92% rename from Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs rename to Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs index bf50633..74d8293 100644 --- a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs +++ b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs @@ -1,6 +1,6 @@ -// +// using System; -using Api.Data; +using CvMatcher.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Api.Migrations +namespace CvMatcher.Data.Migrations { [DbContext(typeof(CvMatcherDbContext))] [Migration("20260524140335_AddLanguageToCvMatchResult")] @@ -26,7 +26,7 @@ namespace Api.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Api.Data.Entities.CvMatchResultEntity", b => + modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -66,7 +66,7 @@ namespace Api.Migrations b.ToTable("Results", "cvMatcher"); }); - modelBuilder.Entity("Api.Data.Entities.CvMatcherChatCacheEntity", b => + modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) diff --git a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.cs b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.cs similarity index 90% rename from Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.cs rename to Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.cs index c711c23..0c58014 100644 --- a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.cs +++ b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.cs @@ -1,8 +1,8 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Api.Migrations +namespace CvMatcher.Data.Migrations { /// public partial class AddLanguageToCvMatchResult : Migration diff --git a/Apis/cv-matcher-api/Migrations/CvMatcherDbContextModelSnapshot.cs b/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs similarity index 92% rename from Apis/cv-matcher-api/Migrations/CvMatcherDbContextModelSnapshot.cs rename to Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs index af0e900..8e7ffff 100644 --- a/Apis/cv-matcher-api/Migrations/CvMatcherDbContextModelSnapshot.cs +++ b/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ -// +// using System; -using Api.Data; +using CvMatcher.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Api.Migrations +namespace CvMatcher.Data.Migrations { [DbContext(typeof(CvMatcherDbContext))] partial class CvMatcherDbContextModelSnapshot : ModelSnapshot @@ -23,7 +23,7 @@ namespace Api.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Api.Data.Entities.CvMatchResultEntity", b => + modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -63,7 +63,7 @@ namespace Api.Migrations b.ToTable("Results", "cvMatcher"); }); - modelBuilder.Entity("Api.Data.Entities.CvMatcherChatCacheEntity", b => + modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) diff --git a/Apis/cv-matcher-data/cv-matcher-data.csproj b/Apis/cv-matcher-data/cv-matcher-data.csproj new file mode 100644 index 0000000..3e627b1 --- /dev/null +++ b/Apis/cv-matcher-data/cv-matcher-data.csproj @@ -0,0 +1,19 @@ + + + net10.0 + enable + enable + cv-matcher-data + CvMatcher.Data + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Apis/cv-search-data/Data/CvSearchDbContext.cs b/Apis/cv-search-data/Data/CvSearchDbContext.cs new file mode 100644 index 0000000..0c66516 --- /dev/null +++ b/Apis/cv-search-data/Data/CvSearchDbContext.cs @@ -0,0 +1,62 @@ +using CvSearch.Data.Entities; +using Microsoft.EntityFrameworkCore; + +namespace CvSearch.Data; + +public sealed class CvSearchDbContext : DbContext +{ + public const string SchemaName = "cvSearch"; + public const string MigrationTableName = "_Migrations"; + + public CvSearchDbContext(DbContextOptions options) : base(options) { } + + public DbSet JobSearchTokens => Set(); + public DbSet JobSearchSessions => Set(); + public DbSet JobSearchResults => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema(SchemaName); + + modelBuilder.Entity(entity => + { + entity.ToTable("JobSearchTokens"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Id).HasMaxLength(64); + entity.Property(x => x.CvDocumentId).HasMaxLength(64).IsRequired(); + entity.Property(x => x.Email).HasMaxLength(256).IsRequired(); + entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired(); + entity.Property(x => x.Used).HasDefaultValue(false); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("JobSearchSessions"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Id).HasMaxLength(64); + entity.Property(x => x.TokenId).HasMaxLength(64).IsRequired(); + entity.Property(x => x.CvDocumentId).HasMaxLength(64).IsRequired(); + entity.Property(x => x.Email).HasMaxLength(256).IsRequired(); + entity.Property(x => x.Status).HasMaxLength(32).IsRequired(); + entity.Property(x => x.Keywords).HasMaxLength(1000); + entity.Property(x => x.ProviderConfigJson).IsRequired(false); + entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired(); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()"); + entity.HasIndex(x => x.Status); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("JobSearchResults"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Id).HasMaxLength(64); + entity.Property(x => x.SessionId).HasMaxLength(64).IsRequired(); + entity.Property(x => x.ProviderName).HasMaxLength(128); + entity.Property(x => x.JobUrl).HasMaxLength(2048); + entity.Property(x => x.JobTitle).HasMaxLength(512); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()"); + entity.HasIndex(x => x.SessionId); + }); + } +} diff --git a/Apis/cv-search-data/Data/Entities/JobSearchResultEntity.cs b/Apis/cv-search-data/Data/Entities/JobSearchResultEntity.cs new file mode 100644 index 0000000..f64f4ec --- /dev/null +++ b/Apis/cv-search-data/Data/Entities/JobSearchResultEntity.cs @@ -0,0 +1,14 @@ +using Shared.Data.Entities; + +namespace CvSearch.Data.Entities; + +public sealed class JobSearchResultEntity : BaseEntity +{ + public string SessionId { get; set; } = string.Empty; + public string ProviderName { get; set; } = string.Empty; + public string JobUrl { get; set; } = string.Empty; + public string JobTitle { get; set; } = string.Empty; + public string JobText { get; set; } = string.Empty; + public int Score { get; set; } + public string ResultJson { get; set; } = string.Empty; +} diff --git a/Apis/cv-search-data/Data/Entities/JobSearchSessionEntity.cs b/Apis/cv-search-data/Data/Entities/JobSearchSessionEntity.cs new file mode 100644 index 0000000..70102e4 --- /dev/null +++ b/Apis/cv-search-data/Data/Entities/JobSearchSessionEntity.cs @@ -0,0 +1,22 @@ +using Shared.Data.Entities; + +namespace CvSearch.Data.Entities; + +public sealed class JobSearchSessionEntity : BaseEntity +{ + public string TokenId { get; set; } = string.Empty; + public string CvDocumentId { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Status { get; set; } = JobSearchStatus.Pending; + public string Keywords { get; set; } = string.Empty; + public string? ProviderConfigJson { get; set; } + public string Language { get; set; } = "en"; +} + +public static class JobSearchStatus +{ + public const string Pending = "Pending"; + public const string Processing = "Processing"; + public const string Done = "Done"; + public const string Failed = "Failed"; +} diff --git a/Apis/cv-search-data/Data/Entities/JobSearchTokenEntity.cs b/Apis/cv-search-data/Data/Entities/JobSearchTokenEntity.cs new file mode 100644 index 0000000..e3d768c --- /dev/null +++ b/Apis/cv-search-data/Data/Entities/JobSearchTokenEntity.cs @@ -0,0 +1,12 @@ +using Shared.Data.Entities; + +namespace CvSearch.Data.Entities; + +public sealed class JobSearchTokenEntity : BaseEntity +{ + public string CvDocumentId { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Language { get; set; } = "en"; + public DateTime ExpiresAt { get; set; } + public bool Used { get; set; } +} diff --git a/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.Designer.cs b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.Designer.cs new file mode 100644 index 0000000..26141b2 --- /dev/null +++ b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.Designer.cs @@ -0,0 +1,160 @@ +// +using System; +using CvSearch.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CvSearch.Data.Migrations +{ + [DbContext(typeof(CvSearchDbContext))] + [Migration("20260522093356_AddJobSearchTables")] + partial class AddJobSearchTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("cvSearch") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("JobText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("JobUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ResultJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.ToTable("JobSearchResults", "cvSearch"); + }); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("CvDocumentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Keywords") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProviderConfigJson") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.ToTable("JobSearchSessions", "cvSearch"); + }); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("CvDocumentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Used") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.HasKey("Id"); + + b.ToTable("JobSearchTokens", "cvSearch"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.cs b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.cs new file mode 100644 index 0000000..d57fc57 --- /dev/null +++ b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.cs @@ -0,0 +1,102 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CvSearch.Data.Migrations +{ + /// + public partial class AddJobSearchTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "cvSearch"); + + migrationBuilder.CreateTable( + name: "JobSearchResults", + schema: "cvSearch", + columns: table => new + { + Id = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + SessionId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + ProviderName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + JobUrl = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false), + JobTitle = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false), + JobText = table.Column(type: "nvarchar(max)", nullable: false), + Score = table.Column(type: "int", nullable: false), + ResultJson = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()") + }, + constraints: table => + { + table.PrimaryKey("PK_JobSearchResults", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "JobSearchSessions", + schema: "cvSearch", + columns: table => new + { + Id = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + TokenId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + CvDocumentId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "nvarchar(32)", maxLength: 32, nullable: false), + Keywords = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false), + ProviderConfigJson = table.Column(type: "nvarchar(max)", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()") + }, + constraints: table => + { + table.PrimaryKey("PK_JobSearchSessions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "JobSearchTokens", + schema: "cvSearch", + columns: table => new + { + Id = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + CvDocumentId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: false), + Used = table.Column(type: "bit", nullable: false, defaultValue: false), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()") + }, + constraints: table => + { + table.PrimaryKey("PK_JobSearchTokens", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_JobSearchResults_SessionId", + schema: "cvSearch", + table: "JobSearchResults", + column: "SessionId"); + + migrationBuilder.CreateIndex( + name: "IX_JobSearchSessions_Status", + schema: "cvSearch", + table: "JobSearchSessions", + column: "Status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "JobSearchResults", + schema: "cvSearch"); + + migrationBuilder.DropTable( + name: "JobSearchSessions", + schema: "cvSearch"); + + migrationBuilder.DropTable( + name: "JobSearchTokens", + schema: "cvSearch"); + } + } +} diff --git a/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.Designer.cs b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.Designer.cs new file mode 100644 index 0000000..847c985 --- /dev/null +++ b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.Designer.cs @@ -0,0 +1,174 @@ +// +using System; +using CvSearch.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CvSearch.Data.Migrations +{ + [DbContext(typeof(CvSearchDbContext))] + [Migration("20260524145702_AddLanguageToJobSearchEntities")] + partial class AddLanguageToJobSearchEntities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("cvSearch") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("JobText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("JobUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ResultJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.ToTable("JobSearchResults", "cvSearch"); + }); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("CvDocumentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Keywords") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Language") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("nvarchar(8)") + .HasDefaultValue("en"); + + b.Property("ProviderConfigJson") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.ToTable("JobSearchSessions", "cvSearch"); + }); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("CvDocumentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Language") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("nvarchar(8)") + .HasDefaultValue("en"); + + b.Property("Used") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.HasKey("Id"); + + b.ToTable("JobSearchTokens", "cvSearch"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.cs b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.cs new file mode 100644 index 0000000..ef76ef8 --- /dev/null +++ b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CvSearch.Data.Migrations +{ + /// + public partial class AddLanguageToJobSearchEntities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Language", + schema: "cvSearch", + table: "JobSearchTokens", + type: "nvarchar(8)", + maxLength: 8, + nullable: false, + defaultValue: "en"); + + migrationBuilder.AddColumn( + name: "Language", + schema: "cvSearch", + table: "JobSearchSessions", + type: "nvarchar(8)", + maxLength: 8, + nullable: false, + defaultValue: "en"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Language", + schema: "cvSearch", + table: "JobSearchTokens"); + + migrationBuilder.DropColumn( + name: "Language", + schema: "cvSearch", + table: "JobSearchSessions"); + } + } +} diff --git a/Apis/cv-search-data/Migrations/CvSearchDbContextModelSnapshot.cs b/Apis/cv-search-data/Migrations/CvSearchDbContextModelSnapshot.cs new file mode 100644 index 0000000..1cb9f20 --- /dev/null +++ b/Apis/cv-search-data/Migrations/CvSearchDbContextModelSnapshot.cs @@ -0,0 +1,171 @@ +// +using System; +using CvSearch.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CvSearch.Data.Migrations +{ + [DbContext(typeof(CvSearchDbContext))] + partial class CvSearchDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("cvSearch") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("JobText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("JobUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ResultJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.ToTable("JobSearchResults", "cvSearch"); + }); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("CvDocumentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Keywords") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Language") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("nvarchar(8)") + .HasDefaultValue("en"); + + b.Property("ProviderConfigJson") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.ToTable("JobSearchSessions", "cvSearch"); + }); + + modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("CvDocumentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Language") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("nvarchar(8)") + .HasDefaultValue("en"); + + b.Property("Used") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.HasKey("Id"); + + b.ToTable("JobSearchTokens", "cvSearch"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apis/cv-search-data/cv-search-data.csproj b/Apis/cv-search-data/cv-search-data.csproj new file mode 100644 index 0000000..7209708 --- /dev/null +++ b/Apis/cv-search-data/cv-search-data.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + cv-search-data + CvSearch.Data + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Apis/cv-search-data/cv-search-models.csproj b/Apis/cv-search-data/cv-search-models.csproj new file mode 100644 index 0000000..310b3cf --- /dev/null +++ b/Apis/cv-search-data/cv-search-models.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + CvSearch.Models + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Apis/myai-data/Data/Entities/TemplateEntity.cs b/Apis/myai-data/Data/Entities/TemplateEntity.cs new file mode 100644 index 0000000..154ad43 --- /dev/null +++ b/Apis/myai-data/Data/Entities/TemplateEntity.cs @@ -0,0 +1,11 @@ +namespace MyAi.Data.Entities; + +// composite PK (Key + Language) — BaseEntity not applicable +public sealed class TemplateEntity +{ + public string Key { get; set; } = string.Empty; + public string Language { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public DateTime UpdatedAt { get; set; } +} diff --git a/Apis/myai-data/Data/MyAiDbContext.cs b/Apis/myai-data/Data/MyAiDbContext.cs new file mode 100644 index 0000000..6ceb60b --- /dev/null +++ b/Apis/myai-data/Data/MyAiDbContext.cs @@ -0,0 +1,30 @@ +using MyAi.Data.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MyAi.Data; + +public sealed class MyAiDbContext : DbContext +{ + public const string SchemaName = "myAi"; + public const string MigrationTableName = "_MyAiMigrations"; + + public MyAiDbContext(DbContextOptions options) : base(options) { } + + public DbSet Templates => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema(SchemaName); + + modelBuilder.Entity(entity => + { + entity.ToTable("Templates"); + entity.HasKey(x => new { x.Key, x.Language }); + entity.Property(x => x.Key).HasMaxLength(128); + entity.Property(x => x.Language).HasMaxLength(8); + entity.Property(x => x.Value).IsRequired(); + entity.Property(x => x.Description).HasMaxLength(500).HasDefaultValue(string.Empty); + entity.Property(x => x.UpdatedAt).HasDefaultValueSql("SYSUTCDATETIME()"); + }); + } +} diff --git a/Apis/myai-data/Migrations/20260524145351_AddTemplates.Designer.cs b/Apis/myai-data/Migrations/20260524145351_AddTemplates.Designer.cs new file mode 100644 index 0000000..63cf0c0 --- /dev/null +++ b/Apis/myai-data/Migrations/20260524145351_AddTemplates.Designer.cs @@ -0,0 +1,62 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyAi.Data; + +#nullable disable + +namespace MyAi.Data.Migrations +{ + [DbContext(typeof(MyAiDbContext))] + [Migration("20260524145351_AddTemplates")] + partial class AddTemplates + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("myAi") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MyAi.Data.Entities.TemplateEntity", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Language") + .HasMaxLength(8) + .HasColumnType("nvarchar(8)"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasDefaultValue(""); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key", "Language"); + + b.ToTable("Templates", "myAi"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apis/myai-data/Migrations/20260524145351_AddTemplates.cs b/Apis/myai-data/Migrations/20260524145351_AddTemplates.cs new file mode 100644 index 0000000..36f47f1 --- /dev/null +++ b/Apis/myai-data/Migrations/20260524145351_AddTemplates.cs @@ -0,0 +1,113 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyAi.Data.Migrations +{ + /// + public partial class AddTemplates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "myAi"); + + migrationBuilder.CreateTable( + name: "Templates", + schema: "myAi", + columns: table => new + { + Key = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + Language = table.Column(type: "nvarchar(8)", maxLength: 8, nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false, defaultValue: ""), + UpdatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()") + }, + constraints: table => + { + table.PrimaryKey("PK_Templates", x => new { x.Key, x.Language }); + }); + + Seed(migrationBuilder); + } + + private static void Seed(MigrationBuilder m) + { + void Row(string key, string lang, string value, string description = "") + => m.InsertData("Templates", ["Key", "Language", "Value", "Description"], [key, lang, value, description], "myAi"); + + // Match result email — subject + Row("email.match.subject", "en", "MyAi.ro CV Match: {{score}}% - {{jobLabel}}", "Subject for the CV match result email"); + Row("email.match.subject", "ro", "MyAi.ro Potrivire CV: {{score}}% - {{jobLabel}}", "Subiect email rezultat potrivire CV"); + + // Match result email — body + Row("email.match.body", "en", + "CV Matcher result\n\nCV Document ID: {{cvDocumentId}}\nJob: {{jobLabel}}\nJob URL: {{jobUrl}}\nScore: {{score}}%\n\nSummary:\n{{summary}}\n\nStrengths:\n{{strengths}}\n\nGaps:\n{{gaps}}\n\nRecommendations:\n{{recommendations}}", + "Body for the CV match result email"); + Row("email.match.body", "ro", + "Rezultat potrivire CV\n\nID document CV: {{cvDocumentId}}\nJob: {{jobLabel}}\nURL job: {{jobUrl}}\nScor: {{score}}%\n\nRezumat:\n{{summary}}\n\nPuncte forte:\n{{strengths}}\n\nLipsuri:\n{{gaps}}\n\nRecomandări:\n{{recommendations}}", + "Corpul emailului pentru rezultatul potrivirii CV"); + + // Match result email — job search CTA footer + Row("email.match.job-search-footer", "en", + "\n\n---\nWant to find more jobs matching your CV?\nClick: {{jobSearchLink}}\n(link valid for {{expiryDays}} days)", + "Job search CTA appended to match result email"); + Row("email.match.job-search-footer", "ro", + "\n\n---\nVrei sa gasesti mai multe joburi potrivite CV-ului tau?\nClick: {{jobSearchLink}}\n(link valabil {{expiryDays}} zile)", + "CTA cautare joburi adaugat la emailul de potrivire CV"); + + // Job search results email — subject + Row("email.search-results.subject", "en", "MyAi.ro: {{count}} jobs matching your CV", "Subject for job search results email"); + Row("email.search-results.subject", "ro", "MyAi.ro: {{count}} joburi potrivite CV-ului tau", "Subiect email rezultate cautare joburi"); + + // Job search results email — body preamble (items appended in code) + Row("email.search-results.body", "en", "MyAi.ro found {{count}} jobs matching your CV:\n\n{{items}}", "Body preamble for job search results email"); + Row("email.search-results.body", "ro", "MyAi.ro a gasit {{count}} joburi potrivite CV-ului tau:\n\n{{items}}", "Corpul emailului de rezultate cautare joburi"); + + // Job search results email — no results found + Row("email.search-results.empty", "en", "MyAi.ro found no jobs matching your CV. Try again later or update your CV.", "No results message for job search results email"); + Row("email.search-results.empty", "ro", "MyAi.ro nu a gasit joburi care sa corespunda CV-ului tau. Incercati mai tarziu sau ajustati CV-ul.", "Mesaj fara rezultate pentru emailul de cautare joburi"); + + // HTML job-search start page messages + Row("html.job-search.started.title", "en", "Job search started", "Title for job search started page"); + Row("html.job-search.started.message", "en", "Your job search has started. Results will be sent to your email shortly.", "Message for job search started page"); + Row("html.job-search.started.title", "ro", "Căutare joburi pornită", "Titlu pagina cautare joburi pornita"); + Row("html.job-search.started.message", "ro", "Căutarea joburilor a început. Rezultatele vor fi trimise pe email în scurt timp.", "Mesaj pagina cautare joburi pornita"); + + Row("html.job-search.already-used.title", "en", "Link already used", "Title for already-used page"); + Row("html.job-search.already-used.message", "en", "This job search link has already been used.", "Message for already-used page"); + Row("html.job-search.already-used.title", "ro", "Link deja folosit", "Titlu pagina link deja folosit"); + Row("html.job-search.already-used.message", "ro", "Acest link de cautare joburi a fost deja folosit.", "Mesaj pagina link deja folosit"); + + Row("html.job-search.expired.title", "en", "Link expired", "Title for expired link page"); + Row("html.job-search.expired.message", "en", "This job search link has expired. Please request a new CV match to get a fresh link.", "Message for expired link page"); + Row("html.job-search.expired.title", "ro", "Link expirat", "Titlu pagina link expirat"); + Row("html.job-search.expired.message", "ro", "Acest link de cautare joburi a expirat. Solicita o noua potrivire CV pentru a primi un link nou.", "Mesaj pagina link expirat"); + + Row("html.job-search.invalid.title", "en", "Invalid link", "Title for invalid link page"); + Row("html.job-search.invalid.message", "en", "This job search link is not valid.", "Message for invalid link page"); + Row("html.job-search.invalid.title", "ro", "Link invalid", "Titlu pagina link invalid"); + Row("html.job-search.invalid.message", "ro", "Acest link de cautare joburi nu este valid.", "Mesaj pagina link invalid"); + + Row("html.job-search.error.title", "en", "Error", "Title for error page"); + Row("html.job-search.error.message", "en", "An error occurred. Please try again later.", "Message for error page"); + Row("html.job-search.error.title", "ro", "Eroare", "Titlu pagina eroare"); + Row("html.job-search.error.message", "ro", "A apărut o eroare. Te rugăm să încerci din nou mai târziu.", "Mesaj pagina eroare"); + + // AI system prompt for CV matching (language is a {{languageName}} variable inside it) + Row("ai.cv-match.system-prompt", "*", + "You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100.\nPenalize missing required skills. Do not invent experience. Use concise business language.\nRespond entirely in {{languageName}} — all text fields in the JSON must be in {{languageName}}.\nJSON shape: {\"score\":number,\"summary\":\"...\",\"strengths\":[\"...\"],\"gaps\":[\"...\"],\"recommendations\":[\"...\"],\"evidence\":[\"...\"]}", + "System prompt template for the CV-to-job LLM matching call. {{languageName}} is substituted at runtime."); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Templates", + schema: "myAi"); + } + } +} diff --git a/Apis/myai-data/Migrations/MyAiDbContextModelSnapshot.cs b/Apis/myai-data/Migrations/MyAiDbContextModelSnapshot.cs new file mode 100644 index 0000000..71e85d6 --- /dev/null +++ b/Apis/myai-data/Migrations/MyAiDbContextModelSnapshot.cs @@ -0,0 +1,59 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyAi.Data; + +#nullable disable + +namespace MyAi.Data.Migrations +{ + [DbContext(typeof(MyAiDbContext))] + partial class MyAiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("myAi") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MyAi.Data.Entities.TemplateEntity", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Language") + .HasMaxLength(8) + .HasColumnType("nvarchar(8)"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasDefaultValue(""); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSUTCDATETIME()"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key", "Language"); + + b.ToTable("Templates", "myAi"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apis/myai-data/Services/DbTemplateService.cs b/Apis/myai-data/Services/DbTemplateService.cs new file mode 100644 index 0000000..aa9bd50 --- /dev/null +++ b/Apis/myai-data/Services/DbTemplateService.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MyAi.Data; +using System.Collections.Concurrent; + +namespace MyAi.Data.Services; + +public sealed class DbTemplateService : ITemplateService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); + private DateTime _loadedAt = DateTime.MinValue; + private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10); + + public DbTemplateService(IServiceScopeFactory scopeFactory, ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + public string Get(string key, string language = "en") + { + EnsureCacheLoaded(); + + if (_cache.TryGetValue(CacheKey(key, language), out var value)) + return value; + + if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) + && _cache.TryGetValue(CacheKey(key, "en"), out var fallback)) + return fallback; + + _logger.LogWarning("Template not found: key={Key}, language={Language}", key, language); + return key; + } + + public string Render(string key, string language, params (string Key, string Value)[] placeholders) + { + var template = Get(key, language); + foreach (var (k, v) in placeholders) + template = template.Replace($"{{{{{k}}}}}", v, StringComparison.OrdinalIgnoreCase); + return template; + } + + private void EnsureCacheLoaded() + { + if (DateTime.UtcNow - _loadedAt < CacheTtl) return; + + try + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var rows = db.Templates.AsNoTracking().ToList(); + var fresh = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + foreach (var row in rows) + fresh[CacheKey(row.Key, row.Language)] = row.Value; + + _cache = fresh; + _loadedAt = DateTime.UtcNow; + _logger.LogDebug("Template cache refreshed. {Count} templates loaded.", rows.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh template cache. Serving stale cache."); + } + } + + private static string CacheKey(string key, string language) => $"{key}::{language}"; +} diff --git a/Apis/myai-data/Services/ITemplateService.cs b/Apis/myai-data/Services/ITemplateService.cs new file mode 100644 index 0000000..1c4f239 --- /dev/null +++ b/Apis/myai-data/Services/ITemplateService.cs @@ -0,0 +1,7 @@ +namespace MyAi.Data.Services; + +public interface ITemplateService +{ + string Get(string key, string language = "en"); + string Render(string key, string language, params (string Key, string Value)[] placeholders); +} diff --git a/Apis/myai-data/myai-data.csproj b/Apis/myai-data/myai-data.csproj new file mode 100644 index 0000000..4095db1 --- /dev/null +++ b/Apis/myai-data/myai-data.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + myai-data + MyAi.Data + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Apis/myai-data/myai-models.csproj b/Apis/myai-data/myai-models.csproj new file mode 100644 index 0000000..cf8d4c5 --- /dev/null +++ b/Apis/myai-data/myai-models.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + MyAi.Models + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Apis/rag-api-models/Settings/OllamaSettings.cs b/Apis/rag-api-models/Settings/OllamaSettings.cs index b75ffe2..e99b845 100644 --- a/Apis/rag-api-models/Settings/OllamaSettings.cs +++ b/Apis/rag-api-models/Settings/OllamaSettings.cs @@ -1,6 +1,6 @@ namespace Rag.Models.Settings; -public sealed class OllamaSettings : Shared.Models.Settings.OllamaSettings +public sealed class OllamaSettings : Common.Settings.OllamaSettings { public string EmbeddingModel { get; set; } = "nomic-embed-text"; } diff --git a/Apis/rag-api-models/Settings/OpenAiSettings.cs b/Apis/rag-api-models/Settings/OpenAiSettings.cs index b1c7f36..80eccbc 100644 --- a/Apis/rag-api-models/Settings/OpenAiSettings.cs +++ b/Apis/rag-api-models/Settings/OpenAiSettings.cs @@ -1,6 +1,6 @@ namespace Rag.Models.Settings; -public sealed class OpenAiSettings: Shared.Models.Settings.OpenAiSettings +public sealed class OpenAiSettings : Common.Settings.OpenAiSettings { public string EmbeddingModel { get; set; } = "text-embedding-3-small"; } diff --git a/Apis/rag-api-models/rag-api-models.csproj b/Apis/rag-api-models/rag-api-models.csproj index b19eedd..d5098be 100644 --- a/Apis/rag-api-models/rag-api-models.csproj +++ b/Apis/rag-api-models/rag-api-models.csproj @@ -8,7 +8,7 @@ - + diff --git a/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs b/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs index 4821285..0aa29b2 100644 --- a/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs +++ b/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Options; using Rag.Models.Settings; -using Api.Data.Repositories.Contracts; +using Rag.Data.Repositories.Contracts; using Api.Clients.Ai.Contracts; using CommonHelpers; diff --git a/Apis/rag-api/Controllers/RagController.cs b/Apis/rag-api/Controllers/RagController.cs index ecf07c6..d3e8cfc 100644 --- a/Apis/rag-api/Controllers/RagController.cs +++ b/Apis/rag-api/Controllers/RagController.cs @@ -3,7 +3,7 @@ using Api.Services.Contracts; using Rag.Models.Requests; using Rag.Models.Responses; using Swashbuckle.AspNetCore.Annotations; -using Shared.Models.Responses; +using Common.Responses; namespace Api.Controllers; diff --git a/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs b/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs index 2837287..4993761 100644 --- a/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs +++ b/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs @@ -1,6 +1,6 @@ using Rag.Models; -namespace Api.Data.Repositories.Contracts; +namespace Rag.Data.Repositories.Contracts; public interface IRagRepository { diff --git a/Apis/rag-api/Data/Repositories/EfRagRepository.cs b/Apis/rag-api/Data/Repositories/EfRagRepository.cs index e07b4ee..e644599 100644 --- a/Apis/rag-api/Data/Repositories/EfRagRepository.cs +++ b/Apis/rag-api/Data/Repositories/EfRagRepository.cs @@ -1,10 +1,10 @@ -using Api.Data; -using Api.Data.Entities; +using Rag.Data; +using Rag.Data.Entities; using Microsoft.EntityFrameworkCore; -using Api.Data.Repositories.Contracts; +using Rag.Data.Repositories.Contracts; using Rag.Models; -namespace Api.Data.Repositories; +namespace Rag.Data.Repositories; public sealed class EfRagRepository : IRagRepository { diff --git a/Apis/rag-api/Data/Repositories/VectorSerializer.cs b/Apis/rag-api/Data/Repositories/VectorSerializer.cs index 2ed02c6..c70d2f3 100644 --- a/Apis/rag-api/Data/Repositories/VectorSerializer.cs +++ b/Apis/rag-api/Data/Repositories/VectorSerializer.cs @@ -1,4 +1,4 @@ -namespace Api.Data.Repositories; +namespace Rag.Data.Repositories; public static class VectorSerializer { diff --git a/Apis/rag-api/Dockerfile b/Apis/rag-api/Dockerfile index 9878095..5284fbd 100644 --- a/Apis/rag-api/Dockerfile +++ b/Apis/rag-api/Dockerfile @@ -3,16 +3,20 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY Apis/rag-api/rag-api.csproj Apis/rag-api/ -COPY Apis/shared-models/shared-models.csproj Apis/shared-models/ +COPY Apis/rag-data/rag-data.csproj Apis/rag-data/ +COPY Apis/common/common.csproj Apis/common/ COPY Apis/rag-api-models/rag-api-models.csproj Apis/rag-api-models/ +COPY Apis/shared-data/shared-data.csproj Apis/shared-data/ COPY Helpers/common-helpers/common-helpers.csproj Helpers/common-helpers/ COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/ RUN dotnet restore Apis/rag-api/rag-api.csproj COPY Apis/rag-api/ Apis/rag-api/ -COPY Apis/shared-models/ Apis/shared-models/ +COPY Apis/rag-data/ Apis/rag-data/ +COPY Apis/common/ Apis/common/ COPY Apis/rag-api-models/ Apis/rag-api-models/ +COPY Apis/shared-data/ Apis/shared-data/ COPY Helpers/common-helpers/ Helpers/common-helpers/ COPY Helpers/startup-helpers/ Helpers/startup-helpers/ @@ -25,4 +29,4 @@ ENV ASPNETCORE_URLS=http://0.0.0.0:8080 COPY --from=build /app/publish . -ENTRYPOINT ["dotnet", "rag-api.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "rag-api.dll"] diff --git a/Apis/rag-api/Program.cs b/Apis/rag-api/Program.cs index daadebf..a269169 100644 --- a/Apis/rag-api/Program.cs +++ b/Apis/rag-api/Program.cs @@ -1,15 +1,15 @@ using System.Reflection; using Api.Clients.Ai; using Api.Clients.Ai.Contracts; -using Api.Data; -using Api.Data.Repositories; -using Api.Data.Repositories.Contracts; +using Rag.Data; +using Rag.Data.Repositories; +using Rag.Data.Repositories.Contracts; using Api.Services; using Api.Services.Contracts; using Microsoft.EntityFrameworkCore; using Rag.Models.Settings; using Serilog; -using Shared.Models.Settings; +using Common.Settings; using StartupHelpers; StartupExtensions.LoadDotEnvFile(); @@ -39,11 +39,10 @@ try options.UseSqlServer(connectionString, sql => { sql.MigrationsHistoryTable(RagDbContext.MigrationTableName, RagDbContext.SchemaName); + sql.MigrationsAssembly("rag-data"); }); }); - builder.Services.AddHttpClient(); - builder.Services.AddScoped(); builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Apis/rag-api/Services/RagService.cs b/Apis/rag-api/Services/RagService.cs index 9799feb..9a8eab2 100644 --- a/Apis/rag-api/Services/RagService.cs +++ b/Apis/rag-api/Services/RagService.cs @@ -4,7 +4,7 @@ using Api.Services.Contracts; using Rag.Models.Requests; using Rag.Models.Responses; using Rag.Models.Settings; -using Api.Data.Repositories.Contracts; +using Rag.Data.Repositories.Contracts; using Api.Clients.Ai.Contracts; using Rag.Models; using CommonHelpers; diff --git a/Apis/rag-api/rag-api.csproj b/Apis/rag-api/rag-api.csproj index 91f151b..d2c4dba 100644 --- a/Apis/rag-api/rag-api.csproj +++ b/Apis/rag-api/rag-api.csproj @@ -58,28 +58,29 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + + - - - + + + + diff --git a/Apis/rag-api/Data/Entities/RagChatCompletionCacheEntity.cs b/Apis/rag-data/Entities/RagChatCompletionCacheEntity.cs similarity index 81% rename from Apis/rag-api/Data/Entities/RagChatCompletionCacheEntity.cs rename to Apis/rag-data/Entities/RagChatCompletionCacheEntity.cs index 05940b9..c873e60 100644 --- a/Apis/rag-api/Data/Entities/RagChatCompletionCacheEntity.cs +++ b/Apis/rag-data/Entities/RagChatCompletionCacheEntity.cs @@ -1,5 +1,6 @@ -namespace Api.Data.Entities; +namespace Rag.Data.Entities; +// CacheKey PK — BaseEntity not applicable public sealed class RagChatCompletionCacheEntity { public string CacheKey { get; set; } = string.Empty; diff --git a/Apis/rag-api/Data/Entities/RagChunkEntity.cs b/Apis/rag-data/Entities/RagChunkEntity.cs similarity index 78% rename from Apis/rag-api/Data/Entities/RagChunkEntity.cs rename to Apis/rag-data/Entities/RagChunkEntity.cs index b57467c..6bd1734 100644 --- a/Apis/rag-api/Data/Entities/RagChunkEntity.cs +++ b/Apis/rag-data/Entities/RagChunkEntity.cs @@ -1,5 +1,6 @@ -namespace Api.Data.Entities; +namespace Rag.Data.Entities; +// no CreatedAt column in schema — BaseEntity not applicable public sealed class RagChunkEntity { public string Id { get; set; } = string.Empty; diff --git a/Apis/rag-api/Data/Entities/RagDocumentEntity.cs b/Apis/rag-data/Entities/RagDocumentEntity.cs similarity index 70% rename from Apis/rag-api/Data/Entities/RagDocumentEntity.cs rename to Apis/rag-data/Entities/RagDocumentEntity.cs index 739af12..7b09463 100644 --- a/Apis/rag-api/Data/Entities/RagDocumentEntity.cs +++ b/Apis/rag-data/Entities/RagDocumentEntity.cs @@ -1,8 +1,9 @@ -namespace Api.Data.Entities; +using Shared.Data.Entities; -public sealed class RagDocumentEntity +namespace Rag.Data.Entities; + +public sealed class RagDocumentEntity : BaseEntity { - public string Id { get; set; } = string.Empty; public string DocumentType { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string? SourceUrl { get; set; } @@ -10,7 +11,6 @@ public sealed class RagDocumentEntity public string TextHash { get; set; } = string.Empty; public double TypeConfidence { get; set; } public string MetadataJson { get; set; } = "{}"; - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public ICollection Chunks { get; set; } = []; } diff --git a/Apis/rag-api/Data/Entities/RagEmbeddingCacheEntity.cs b/Apis/rag-data/Entities/RagEmbeddingCacheEntity.cs similarity index 81% rename from Apis/rag-api/Data/Entities/RagEmbeddingCacheEntity.cs rename to Apis/rag-data/Entities/RagEmbeddingCacheEntity.cs index 63f8132..e96c433 100644 --- a/Apis/rag-api/Data/Entities/RagEmbeddingCacheEntity.cs +++ b/Apis/rag-data/Entities/RagEmbeddingCacheEntity.cs @@ -1,5 +1,6 @@ -namespace Api.Data.Entities; +namespace Rag.Data.Entities; +// CacheKey PK — BaseEntity not applicable public sealed class RagEmbeddingCacheEntity { public string CacheKey { get; set; } = string.Empty; diff --git a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.Designer.cs b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.Designer.cs similarity index 92% rename from Apis/rag-api/Migrations/20260507140305_InitialRagSchema.Designer.cs rename to Apis/rag-data/Migrations/20260507140305_InitialRagSchema.Designer.cs index 3fcaaae..54c078e 100644 --- a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.Designer.cs +++ b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.Designer.cs @@ -1,6 +1,6 @@ -// +// using System; -using Api.Data; +using Rag.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Api.Migrations +namespace Rag.Data.Migrations { [DbContext(typeof(RagDbContext))] [Migration("20260507140305_InitialRagSchema")] @@ -26,7 +26,7 @@ namespace Api.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Api.Data.Entities.RagChatCompletionCacheEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagChatCompletionCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) @@ -54,7 +54,7 @@ namespace Api.Migrations b.ToTable("ChatCompletionCache", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -83,7 +83,7 @@ namespace Api.Migrations b.ToTable("Chunks", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -135,7 +135,7 @@ namespace Api.Migrations b.ToTable("Documents", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagEmbeddingCacheEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagEmbeddingCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) @@ -167,9 +167,9 @@ namespace Api.Migrations b.ToTable("EmbeddingCache", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b => { - b.HasOne("Api.Data.Entities.RagDocumentEntity", "Document") + b.HasOne("Rag.Data.Entities.RagDocumentEntity", "Document") .WithMany("Chunks") .HasForeignKey("DocumentId") .OnDelete(DeleteBehavior.Cascade) @@ -178,7 +178,7 @@ namespace Api.Migrations b.Navigation("Document"); }); - modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b => { b.Navigation("Chunks"); }); diff --git a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.cs b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.cs similarity index 99% rename from Apis/rag-api/Migrations/20260507140305_InitialRagSchema.cs rename to Apis/rag-data/Migrations/20260507140305_InitialRagSchema.cs index fc6216c..48b6c3e 100644 --- a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.cs +++ b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.cs @@ -1,9 +1,9 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Api.Migrations +namespace Rag.Data.Migrations { /// public partial class InitialRagSchema : Migration diff --git a/Apis/rag-api/Migrations/RagDbContextModelSnapshot.cs b/Apis/rag-data/Migrations/RagDbContextModelSnapshot.cs similarity index 92% rename from Apis/rag-api/Migrations/RagDbContextModelSnapshot.cs rename to Apis/rag-data/Migrations/RagDbContextModelSnapshot.cs index 908ae81..a409235 100644 --- a/Apis/rag-api/Migrations/RagDbContextModelSnapshot.cs +++ b/Apis/rag-data/Migrations/RagDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ -// +// using System; -using Api.Data; +using Rag.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace Api.Migrations +namespace Rag.Data.Migrations { [DbContext(typeof(RagDbContext))] partial class RagDbContextModelSnapshot : ModelSnapshot @@ -23,7 +23,7 @@ namespace Api.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Api.Data.Entities.RagChatCompletionCacheEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagChatCompletionCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) @@ -51,7 +51,7 @@ namespace Api.Migrations b.ToTable("ChatCompletionCache", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -80,7 +80,7 @@ namespace Api.Migrations b.ToTable("Chunks", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b => { b.Property("Id") .HasMaxLength(64) @@ -132,7 +132,7 @@ namespace Api.Migrations b.ToTable("Documents", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagEmbeddingCacheEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagEmbeddingCacheEntity", b => { b.Property("CacheKey") .HasMaxLength(64) @@ -164,9 +164,9 @@ namespace Api.Migrations b.ToTable("EmbeddingCache", "rag"); }); - modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b => { - b.HasOne("Api.Data.Entities.RagDocumentEntity", "Document") + b.HasOne("Rag.Data.Entities.RagDocumentEntity", "Document") .WithMany("Chunks") .HasForeignKey("DocumentId") .OnDelete(DeleteBehavior.Cascade) @@ -175,7 +175,7 @@ namespace Api.Migrations b.Navigation("Document"); }); - modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b => + modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b => { b.Navigation("Chunks"); }); diff --git a/Apis/rag-api/Data/RagDbContext.cs b/Apis/rag-data/RagDbContext.cs similarity index 98% rename from Apis/rag-api/Data/RagDbContext.cs rename to Apis/rag-data/RagDbContext.cs index 1564ef1..bd5b85a 100644 --- a/Apis/rag-api/Data/RagDbContext.cs +++ b/Apis/rag-data/RagDbContext.cs @@ -1,7 +1,7 @@ -using Api.Data.Entities; +using Rag.Data.Entities; using Microsoft.EntityFrameworkCore; -namespace Api.Data; +namespace Rag.Data; public sealed class RagDbContext : DbContext { diff --git a/Apis/rag-data/rag-data.csproj b/Apis/rag-data/rag-data.csproj new file mode 100644 index 0000000..207d81d --- /dev/null +++ b/Apis/rag-data/rag-data.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + rag-data + Rag.Data + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Apis/shared-data/Entities/BaseEntity.cs b/Apis/shared-data/Entities/BaseEntity.cs new file mode 100644 index 0000000..05fd6c8 --- /dev/null +++ b/Apis/shared-data/Entities/BaseEntity.cs @@ -0,0 +1,12 @@ +namespace Shared.Data.Entities; + +/// +/// Abstract base for all EF entities that carry a surrogate string PK and an audit timestamp. +/// Entities with a composite PK or a non-Id primary key should NOT inherit this class; +/// document the exception with a brief comment on the entity. +/// +public abstract class BaseEntity +{ + public required string Id { get; init; } + public DateTime CreatedAt { get; init; } +} diff --git a/Apis/shared-data/shared-data.csproj b/Apis/shared-data/shared-data.csproj new file mode 100644 index 0000000..606ae95 --- /dev/null +++ b/Apis/shared-data/shared-data.csproj @@ -0,0 +1,9 @@ + + + net10.0 + shared-data + Shared.Data + enable + enable + + diff --git a/CLAUDE.md b/CLAUDE.md index 52b200b..27a2b35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,29 +39,55 @@ This applies to both the staging and production repos as appropriate. - 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 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. + 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. - shared-models/ Cross-service shared models (DatabaseSettings, etc.). + 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. -web/ Razor Pages / Blazor front-end (port 5000). + 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 @@ -79,27 +105,41 @@ Config lives in `docker-compose/.env`. All env vars use `${VAR:-default}` fallba ## Database schemas -| Schema | Owner DbContext | Migrations assembly | -|-------------|----------------------|-----------------------| -| `cvMatcher` | `CvMatcherDbContext` | `cv-matcher-api` | -| `rag` | `RagDbContext` | `rag-api` | -| `cvSearch` | `CvSearchDbContext` | `cv-search-models` | +| 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 -# Add a migration to cv-search-models -dotnet ef migrations add \ - --context CvSearchDbContext \ - --project Apis/cv-search-models \ +# cv-matcher-data (schema: cvMatcher) +dotnet ef migrations add ` + --context CvMatcherDbContext ` + --project Apis/cv-matcher-data ` --startup-project Apis/cv-matcher-api -# Add a migration to cv-matcher-api -dotnet ef migrations add \ - --context CvMatcherDbContext \ - --project Apis/cv-matcher-api +# rag-data (schema: rag) +dotnet ef migrations add ` + --context RagDbContext ` + --project Apis/rag-data ` + --startup-project Apis/rag-api + +# cv-search-data (schema: cvSearch) +dotnet ef migrations add ` + --context CvSearchDbContext ` + --project Apis/cv-search-data ` + --startup-project Apis/cv-matcher-api + +# myai-data (schema: myAi) +dotnet ef migrations add ` + --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. diff --git a/CV.pdf b/CV.pdf new file mode 100644 index 0000000..2c81179 Binary files /dev/null and b/CV.pdf differ diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..7e06527 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,38 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Helpers/startup-helpers/DatabaseExtensions.cs b/Helpers/startup-helpers/DatabaseExtensions.cs index abe85e3..07c7b60 100644 --- a/Helpers/startup-helpers/DatabaseExtensions.cs +++ b/Helpers/startup-helpers/DatabaseExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Shared.Models.Settings; +using Common.Settings; namespace StartupHelpers; diff --git a/Helpers/startup-helpers/RateLimitingExtensions.cs b/Helpers/startup-helpers/RateLimitingExtensions.cs index 03e0a6a..5087c4c 100644 --- a/Helpers/startup-helpers/RateLimitingExtensions.cs +++ b/Helpers/startup-helpers/RateLimitingExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Shared.Models.Settings; +using Common.Settings; namespace StartupHelpers; diff --git a/Helpers/startup-helpers/startup-helpers.csproj b/Helpers/startup-helpers/startup-helpers.csproj index 00b9f4a..89d7a7c 100644 --- a/Helpers/startup-helpers/startup-helpers.csproj +++ b/Helpers/startup-helpers/startup-helpers.csproj @@ -12,19 +12,19 @@ - - - - - - - - - + + + + + + + + + - + diff --git a/Jobs/cv-cleanup-job-models/cv-cleanup-job-models.csproj b/Jobs/cv-cleanup-job-models/cv-cleanup-job-models.csproj new file mode 100644 index 0000000..1d2bd02 --- /dev/null +++ b/Jobs/cv-cleanup-job-models/cv-cleanup-job-models.csproj @@ -0,0 +1,9 @@ + + + net10.0 + enable + enable + cv-cleanup-job-models + CvCleanup.Job.Models + + diff --git a/Jobs/cv-cleanup-job/Dockerfile b/Jobs/cv-cleanup-job/Dockerfile index 393a490..d2485e1 100644 --- a/Jobs/cv-cleanup-job/Dockerfile +++ b/Jobs/cv-cleanup-job/Dockerfile @@ -6,7 +6,7 @@ COPY Jobs/cv-cleanup-job/cv-cleanup-job.csproj Jobs/cv-cleanup-job/ COPY Jobs/job-scheduler/job-scheduler.csproj Jobs/job-scheduler/ COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/ COPY Apis/api-models/api-models.csproj Apis/api-models/ -COPY Apis/shared-models/shared-models.csproj Apis/shared-models/ +COPY Apis/common/common.csproj Apis/common/ RUN dotnet restore Jobs/cv-cleanup-job/cv-cleanup-job.csproj @@ -14,7 +14,7 @@ COPY Jobs/cv-cleanup-job/ Jobs/cv-cleanup-job/ COPY Jobs/job-scheduler/ Jobs/job-scheduler/ COPY Helpers/startup-helpers/ Helpers/startup-helpers/ COPY Apis/api-models/ Apis/api-models/ -COPY Apis/shared-models/ Apis/shared-models/ +COPY Apis/common/ Apis/common/ RUN dotnet publish Jobs/cv-cleanup-job/cv-cleanup-job.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false diff --git a/Jobs/cv-cleanup-job/cv-cleanup-job.csproj b/Jobs/cv-cleanup-job/cv-cleanup-job.csproj index 9e93dfc..4dd9f40 100644 --- a/Jobs/cv-cleanup-job/cv-cleanup-job.csproj +++ b/Jobs/cv-cleanup-job/cv-cleanup-job.csproj @@ -9,7 +9,7 @@ - + diff --git a/Jobs/cv-search-job-models/cv-search-job-models.csproj b/Jobs/cv-search-job-models/cv-search-job-models.csproj new file mode 100644 index 0000000..94be90d --- /dev/null +++ b/Jobs/cv-search-job-models/cv-search-job-models.csproj @@ -0,0 +1,9 @@ + + + net10.0 + enable + enable + cv-search-job-models + CvSearch.Job.Models + + diff --git a/Jobs/cv-search-job/CLAUDE.md b/Jobs/cv-search-job/CLAUDE.md index 788a1e2..2cc09e1 100644 --- a/Jobs/cv-search-job/CLAUDE.md +++ b/Jobs/cv-search-job/CLAUDE.md @@ -86,5 +86,5 @@ Follows the same scheme as `cv-cleanup-job`: ## EF migrations This project runs `CvSearchDbContext.Database.Migrate()` on startup. -Migrations live in `Apis/cv-search-models/Migrations/`. +Migrations live in `Apis/cv-search-data/Migrations/`. To add a migration: see root CLAUDE.md. diff --git a/Jobs/cv-search-job/Dockerfile b/Jobs/cv-search-job/Dockerfile index ada4d68..5da6414 100644 --- a/Jobs/cv-search-job/Dockerfile +++ b/Jobs/cv-search-job/Dockerfile @@ -4,20 +4,22 @@ WORKDIR /src COPY Jobs/cv-search-job/cv-search-job.csproj Jobs/cv-search-job/ COPY Jobs/job-scheduler/job-scheduler.csproj Jobs/job-scheduler/ -COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/ +COPY Apis/cv-search-data/cv-search-data.csproj Apis/cv-search-data/ COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/ -COPY Apis/shared-models/shared-models.csproj Apis/shared-models/ -COPY Apis/myai-models/myai-models.csproj Apis/myai-models/ +COPY Apis/common/common.csproj Apis/common/ +COPY Apis/myai-data/myai-data.csproj Apis/myai-data/ +COPY Apis/shared-data/shared-data.csproj Apis/shared-data/ COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/ RUN dotnet restore Jobs/cv-search-job/cv-search-job.csproj COPY Jobs/cv-search-job/ Jobs/cv-search-job/ COPY Jobs/job-scheduler/ Jobs/job-scheduler/ -COPY Apis/cv-search-models/ Apis/cv-search-models/ +COPY Apis/cv-search-data/ Apis/cv-search-data/ COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/ -COPY Apis/shared-models/ Apis/shared-models/ -COPY Apis/myai-models/ Apis/myai-models/ +COPY Apis/common/ Apis/common/ +COPY Apis/myai-data/ Apis/myai-data/ +COPY Apis/shared-data/ Apis/shared-data/ COPY Helpers/startup-helpers/ Helpers/startup-helpers/ RUN dotnet publish Jobs/cv-search-job/cv-search-job.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false diff --git a/Jobs/cv-search-job/Program.cs b/Jobs/cv-search-job/Program.cs index 00733d5..1bdf24a 100644 --- a/Jobs/cv-search-job/Program.cs +++ b/Jobs/cv-search-job/Program.cs @@ -1,6 +1,6 @@ using System.Reflection; -using CvSearch.Models.Data; -using CvSearch.Models.Settings; +using CvMatcher.Models.Settings; +using CvSearch.Data; using CvSearchJob.Clients; using CvSearchJob.Services; using CvSearchJob.Tasks; @@ -9,11 +9,11 @@ using JobScheduler.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using MyAi.Models.Data; -using MyAi.Models.Services; +using MyAi.Data; +using MyAi.Data.Services; using Refit; using Serilog; -using Shared.Models.Settings; +using Common.Settings; using StartupHelpers; const string ServiceName = "cv-search-job"; @@ -36,7 +36,7 @@ try var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration); options.UseSqlServer(connectionString, sql => { - sql.MigrationsAssembly("cv-search-models"); + sql.MigrationsAssembly("cv-search-data"); sql.MigrationsHistoryTable(CvSearchDbContext.MigrationTableName, CvSearchDbContext.SchemaName); }); }); @@ -58,7 +58,7 @@ try var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration); options.UseSqlServer(connectionString, sql => { - sql.MigrationsAssembly("myai-models"); + sql.MigrationsAssembly("myai-data"); sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName); }); }); diff --git a/Jobs/cv-search-job/Services/CvSearchEmailSender.cs b/Jobs/cv-search-job/Services/CvSearchEmailSender.cs index 6c23120..58ef994 100644 --- a/Jobs/cv-search-job/Services/CvSearchEmailSender.cs +++ b/Jobs/cv-search-job/Services/CvSearchEmailSender.cs @@ -1,11 +1,11 @@ using CvMatcher.Models.Responses; -using CvSearch.Models.Data.Entities; +using CvSearch.Data.Entities; using MailKit.Net.Smtp; using MailKit.Security; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using MimeKit; -using MyAi.Models.Services; +using MyAi.Data.Services; namespace CvSearchJob.Services; diff --git a/Jobs/cv-search-job/Services/HtmlJobSearcher.cs b/Jobs/cv-search-job/Services/HtmlJobSearcher.cs index 1ca539c..fe03132 100644 --- a/Jobs/cv-search-job/Services/HtmlJobSearcher.cs +++ b/Jobs/cv-search-job/Services/HtmlJobSearcher.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Web; -using CvSearch.Models.Settings; +using CvMatcher.Models.Settings; using Microsoft.Extensions.Logging; namespace CvSearchJob.Services; diff --git a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs index d2d50c1..7993736 100644 --- a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs +++ b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs @@ -1,8 +1,8 @@ using System.Text.Json; using CvMatcher.Models.Requests; -using CvSearch.Models.Data; -using CvSearch.Models.Data.Entities; -using CvSearch.Models.Settings; +using CvSearch.Data; +using CvSearch.Data.Entities; +using CvMatcher.Models.Settings; using CvSearchJob.Clients; using CvSearchJob.Services; using JobScheduler.Tasks; diff --git a/Jobs/cv-search-job/cv-search-job.csproj b/Jobs/cv-search-job/cv-search-job.csproj index 097d9fe..7a695c2 100644 --- a/Jobs/cv-search-job/cv-search-job.csproj +++ b/Jobs/cv-search-job/cv-search-job.csproj @@ -9,10 +9,10 @@ - - - - + + + + @@ -21,11 +21,11 @@ - - + + - + diff --git a/Jobs/job-scheduler/job-scheduler.csproj b/Jobs/job-scheduler/job-scheduler.csproj index ddb1c78..9200dc9 100644 --- a/Jobs/job-scheduler/job-scheduler.csproj +++ b/Jobs/job-scheduler/job-scheduler.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/docs/skills/general-dev-workflow.md b/docs/skills/general-dev-workflow.md new file mode 100644 index 0000000..16bfc24 --- /dev/null +++ b/docs/skills/general-dev-workflow.md @@ -0,0 +1,230 @@ +--- +name: general-dev-workflow +description: Structured development workflow for any project. Guides you through Plan → Issue → Implement → Test → PR with checkpoints at each stage to ensure code quality, thorough testing, and well-documented PRs before merge. Use this whenever starting a new feature, bug fix, or change — regardless of tech stack or platform (GitHub, GitLab, Gitea, etc.). Works locally or with remote repositories. +compatibility: Git, GitHub/GitLab/Gitea (auto-detected) +--- + +# General Development Workflow + +This skill guides you through a structured, repeatable development workflow that works across any tech stack and version control platform. The goal is to ensure consistent, high-quality code while reducing context-switching and providing clear checkpoints. + +## The 5-Phase Workflow + +### Phase 1: Plan +**Goal**: Define the change clearly before writing code. + +Start by answering: +- **What** are you building/fixing? (Brief 1-2 sentence summary) +- **Why** does it matter? (Problem it solves, value it adds) +- **Scope**: What's included? What's NOT included? (Define boundaries early) +- **Success criteria**: How will you know it's done? (Tests pass, performance improves, etc.) +- **Dependencies**: Does this require changes elsewhere? Do other features depend on this? +- **Risks**: What could go wrong? (Breaking changes, performance issues, etc.) + +**Output**: A clear plan document or issue description ready for the next phase. + +**Checkpoint**: Does the plan make sense? Is scope realistic? Have you thought through edge cases? + +--- + +### Phase 2: Create Issue (in your VCS) +**Goal**: Formally track the work — and the time spent on it — starting from the moment the plan is approved. + +> ⚠️ **Create the issue only after the plan has been reviewed and approved.** The issue marks the official start of implementation, so its creation timestamp doubles as the implementation start time. Do not create it speculatively while the plan is still being discussed. + +Steps: +1. **Detect your VCS platform**: GitHub, GitLab, Gitea, or local-only? +2. **Create the issue** with: + - Title matching your plan summary + - Description from Phase 1 (what, why, scope, success criteria) + - A `## Time tracking` section at the bottom with `Started: ` — this lets you measure implementation duration later + - Any relevant labels/tags (bug, feature, enhancement, etc.) + - Assignment (if applicable) +3. **For local-only work**: Create a text file or branch-based tracking if you prefer + +**Output**: Issue link (or local tracking document) that you'll reference in commits and in the PR. + +**Checkpoint**: Can someone else understand the work from this issue? Would they know how to verify it's done? + +> 🕐 **Do not close the issue here.** It stays open until the PR is merged (Phase 5). + +--- + +### Phase 3: Implement +**Goal**: Write code that solves the problem defined in the plan. + +**Before you start coding:** +- Create a branch (e.g., `feature/user-auth`, `fix/api-latency`) that references your issue +- Name the branch clearly so it's easy to track what you're working on + +**While coding:** +- Break work into logical, reviewable commits (not one giant commit) +- Write commit messages that explain *why*, not just *what* + - Bad: "fix bug" + - Good: "fix race condition in event handler by adding mutex lock — prevents duplicate events when rapidly clicking" +- Follow the project's code style and conventions +- Write code you'd be comfortable having someone else maintain + +**As you finish sections:** +- Test locally (Phase 4 will be formal testing, but catch obvious issues early) +- If you realize the scope has changed, update your Phase 1 plan and issue + +**Output**: Commits on your branch that are ready for review. + +**Checkpoint**: Does each commit stand alone? Would a reviewer understand why each change was made? + +--- + +### Phase 4: Test +**Goal**: Verify the code works correctly and doesn't break existing functionality. + +**Write tests for your changes:** +- Unit tests for individual functions/methods +- Integration tests if your code touches multiple systems +- Edge case tests (null inputs, boundary conditions, etc.) +- Regression tests to ensure you didn't break something else + +**Run existing tests:** +- Do all tests pass? Fix failures in Phase 3 code +- Check code coverage. Did you test your changes? + +**Manual testing:** +- Does the feature work as described in the plan? +- Did you test the success criteria from Phase 1? +- Test on different inputs/environments if relevant + +**Test results document** (save for Phase 5 PR): +- Which tests were added? +- What's the coverage? (old → new) +- Any manual testing notes +- Known limitations or edge cases not yet tested + +**Output**: Passing tests, code coverage report, test summary. + +**Checkpoint**: Can you confidently say the code works? Are there any untested paths you're worried about? + +--- + +### Phase 5: Create Pull Request +**Goal**: Get code reviewed and merged into `main`, and automatically close the tracking issue on merge. + +**Before opening the PR:** +- Rebase onto the latest `main`/`master` (avoid merge commits if possible) +- Verify all tests still pass +- Check for merge conflicts +- The PR **must target `main`** (or the project's primary branch) — never merge directly without a PR + +**PR Description** (use this template): +``` +## What +Brief summary of the change (one sentence) + +## Why +Problem it solves / value it adds (2-3 sentences) + +## Changes +- Major code change 1 +- Major code change 2 +- (Be specific—help reviewers understand scope) + +## Testing +- Tests added: [list test files or test count] +- Coverage change: [old % → new %] +- Manual testing: [describe what was tested] + +## Risk Assessment +- Breaking changes? [yes/no, if yes: explain migration path] +- Performance impact? [none/minor/significant] +- Closes # + +## Checklist +- [ ] All tests passing +- [ ] Code review ready +- [ ] Documentation updated (if needed) +- [ ] No merge conflicts +``` + +> 🔗 **Always include `Closes #`** in the PR body. On GitHub and Gitea this auto-closes the issue the moment the PR is merged into the target branch — no manual close needed. + +**During review:** +- Respond to feedback promptly +- If changes are requested, make them and push again (don't force-push) +- Ask for clarification if feedback is unclear + +**Merge criteria** (before merging): +- ✅ All tests pass +- ✅ Approved by at least one reviewer (adapt to your team's policy) +- ✅ No unresolved discussions +- ✅ No merge conflicts + +**After the PR is merged:** +- Verify the issue was automatically closed by the `Closes #N` keyword +- If the platform did not auto-close it, close it now and add a comment with the merge commit SHA and the elapsed time (issue `Created` timestamp → merge timestamp) +- **Never close the issue before the PR is merged** — an open issue means work is still in progress + +**Output**: Merged PR with clear history and review comments; issue automatically closed. + +**Checkpoint**: Is the code in `main`? Is the issue closed? Did you record the implementation duration in the issue? + +--- + +## Workflow Summary (Quick Reference) + +| Phase | Input | Output | Checkpoint | +|-------|-------|--------|-----------| +| 1. Plan | Problem description | Approved plan document | Scope and success criteria defined? Plan reviewed and approved? | +| 2. Issue | **Approved** plan | Issue/ticket link + start timestamp | Is it understandable to others? Issue open (not closed)? | +| 3. Implement | Issue/ticket | Commits on feature branch | Are commits logical and well-messaged? | +| 4. Test | Code on branch | Passing tests + coverage report | Are edge cases covered? | +| 5. PR | Tests passing | PR merged into `main`; issue auto-closed via `Closes #N` | Is `main` updated? Is the issue closed? Duration recorded? | + +--- + +## Tips for Success + +**Keep phases focused.** Don't mix planning with implementation. Don't test in Phase 2. This separation helps catch problems early. + +**Commit frequently.** Small commits are easier to review, easier to revert if needed, and easier to understand. Aim for 50-200 lines per commit. + +**Write for your future self.** Six months from now, you'll read your own commits and PRs. Make them clear. + +**Catch blocking issues early.** If Phase 1 reveals something complex or risky, discuss it with your team *before* you code. + +**Adapt to your team.** If your team requires code owners approval, or has a specific PR template, use those instead of the defaults here. The phases stay the same; the details adapt. + +--- + +## For Different Platforms + +**GitHub**: Issues are native. Use GitHub's PR template and auto-linking (`Closes #123`). + +**GitLab**: Merge Requests (MRs) replace PRs. Pipeline status is built-in. Use GitLab's merge request template. + +**Gitea**: Similar to GitHub. Use Gitea's PR templates and linking. + +**Local/No Platform**: Create a `.github` folder locally with your own issue and PR templates. Track work via commit messages and branch names. + +--- + +## Common Questions + +**Q: Can I skip Phase 2 (create issue)?** +A: If you're working solo on a small fix, maybe. But issues are useful for: remembering *why* you made a change, helping others understand work in progress, tracking what's been done, and measuring how long implementation actually takes. Recommended even for solo developers. + +**Q: When exactly should I create the issue?** +A: Only after the plan (Phase 1) has been reviewed and approved. The issue creation timestamp marks the official start of implementation and serves as the start of your time-tracking window. Creating it during planning inflates the recorded duration. + +**Q: When should I close the issue?** +A: Never close it manually before the PR is merged. Use `Closes #N` in the PR body — the platform (GitHub/Gitea) will close it automatically when the PR merges into the target branch. If auto-close doesn't trigger, close it immediately after merge and note the elapsed time. + +**Q: How do I track implementation time?** +A: The issue creation time is your start. The issue close time (= PR merge time) is your end. To make this explicit, add a `## Time tracking` section to the issue body with `Started: ` when you create it. After merge, update or comment with `Completed: — Duration: X hours/days`. + +**Q: What if my plan changes mid-implementation?** +A: Update your issue/plan (Phase 1) to reflect the new scope. Let your team know if scope grew significantly. + +**Q: How big should commits be?** +A: Aim for "one logical change per commit." If you're changing authentication and fixing a typo, those are two commits. A good rule: can you describe the commit in one clear sentence without "and"? + +**Q: Do I need tests for everything?** +A: Write tests for anything that could break. UI color changes? Maybe not. API endpoint behavior? Definitely. If you're unsure, write the test. diff --git a/myAi.sln b/myAi.sln index 9a74df4..690f362 100644 --- a/myAi.sln +++ b/myAi.sln @@ -17,13 +17,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apis", "Apis", "{0FE6558F-2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-matcher-api-models", "Apis\cv-matcher-api-models\cv-matcher-api-models.csproj", "{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-models", "Apis\cv-search-models\cv-search-models.csproj", "{B2C3D4E5-F6A7-4890-BCDE-F01234567890}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api-models", "Apis\api-models\api-models.csproj", "{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rag-api-models", "Apis\rag-api-models\rag-api-models.csproj", "{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "shared-models", "Apis\shared-models\shared-models.csproj", "{185A8BB0-344A-4856-AEB4-213866EB2EE7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "common", "Apis\common\common.csproj", "{185A8BB0-344A-4856-AEB4-213866EB2EE7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Helpers", "Helpers", "{43E9CD21-25B6-4CB4-B94E-5B953B2E1284}" EndProject @@ -39,7 +37,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-job", "Jobs\cv-se EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "job-scheduler", "Jobs\job-scheduler\job-scheduler.csproj", "{A19D2776-B935-BD35-4AB1-3FCE2092805A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "myai-models", "Apis\myai-models\myai-models.csproj", "{3BE2E134-E773-4574-ABDD-175F00E4932E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "shared-data", "Apis\shared-data\shared-data.csproj", "{1B66E492-1830-4229-A8EF-135714BEADA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "myai-data", "Apis\myai-data\myai-data.csproj", "{9582CD83-0B49-4255-9BA6-BC045C3984AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-data", "Apis\cv-search-data\cv-search-data.csproj", "{CFC1AED5-72BF-4E84-92B6-65819A5AC961}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rag-data", "Apis\rag-data\rag-data.csproj", "{31D58517-29D8-46E9-AEAC-F43FDE540590}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-matcher-data", "Apis\cv-matcher-data\cv-matcher-data.csproj", "{92CA82EB-E558-44E7-9185-6FF8B8299C2A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-cleanup-job-models", "Jobs\cv-cleanup-job-models\cv-cleanup-job-models.csproj", "{02DE69CD-19E6-43C0-8916-DB98E5B5CA89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-job-models", "Jobs\cv-search-job-models\cv-search-job-models.csproj", "{069365DB-1916-4C38-A90D-5E909BD9EDD0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Models", "Models", "{A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{D4E5F6A7-B8C9-4012-3456-789ABCDEF012}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -119,18 +133,6 @@ Global {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x64.Build.0 = Release|Any CPU {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x86.ActiveCfg = Release|Any CPU {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x86.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x64.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x86.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x64.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x64.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x86.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x86.Build.0 = Release|Any CPU {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -227,37 +229,115 @@ Global {A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x64.Build.0 = Release|Any CPU {A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x86.ActiveCfg = Release|Any CPU {A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x86.Build.0 = Release|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x64.ActiveCfg = Debug|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x64.Build.0 = Debug|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x86.ActiveCfg = Debug|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x86.Build.0 = Debug|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|Any CPU.Build.0 = Release|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x64.ActiveCfg = Release|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x64.Build.0 = Release|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x86.ActiveCfg = Release|Any CPU - {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x86.Build.0 = Release|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x64.Build.0 = Debug|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x86.Build.0 = Debug|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|Any CPU.Build.0 = Release|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x64.ActiveCfg = Release|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x64.Build.0 = Release|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x86.ActiveCfg = Release|Any CPU + {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x86.Build.0 = Release|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x64.Build.0 = Debug|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x86.Build.0 = Debug|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|Any CPU.Build.0 = Release|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x64.ActiveCfg = Release|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x64.Build.0 = Release|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x86.ActiveCfg = Release|Any CPU + {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x86.Build.0 = Release|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x64.ActiveCfg = Debug|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x64.Build.0 = Debug|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x86.ActiveCfg = Debug|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x86.Build.0 = Debug|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|Any CPU.Build.0 = Release|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x64.ActiveCfg = Release|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x64.Build.0 = Release|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x86.ActiveCfg = Release|Any CPU + {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x86.Build.0 = Release|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x64.ActiveCfg = Debug|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x64.Build.0 = Debug|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x86.ActiveCfg = Debug|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x86.Build.0 = Debug|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|Any CPU.Build.0 = Release|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x64.ActiveCfg = Release|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x64.Build.0 = Release|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x86.ActiveCfg = Release|Any CPU + {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x86.Build.0 = Release|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x64.Build.0 = Debug|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x86.Build.0 = Debug|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|Any CPU.Build.0 = Release|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x64.ActiveCfg = Release|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x64.Build.0 = Release|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x86.ActiveCfg = Release|Any CPU + {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x86.Build.0 = Release|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x64.ActiveCfg = Debug|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x64.Build.0 = Debug|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x86.ActiveCfg = Debug|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x86.Build.0 = Debug|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|Any CPU.Build.0 = Release|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x64.ActiveCfg = Release|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x64.Build.0 = Release|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x86.ActiveCfg = Release|Any CPU + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x86.Build.0 = Release|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x64.Build.0 = Debug|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x86.Build.0 = Debug|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|Any CPU.Build.0 = Release|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x64.ActiveCfg = Release|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x64.Build.0 = Release|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x86.ActiveCfg = Release|Any CPU + {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C} = {0FE6558F-2157-47F2-A835-558416CE0E2B} + {B0A3EAB7-759A-448A-A906-52DF75A70016} = {0FE6558F-2157-47F2-A835-558416CE0E2B} {A63E1C1A-4A78-49F4-9F5C-D43783294861} = {0FE6558F-2157-47F2-A835-558416CE0E2B} {C40F5025-B0A6-4B25-B4A2-7EA568E06C40} = {0FE6558F-2157-47F2-A835-558416CE0E2B} - {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1} = {0FE6558F-2157-47F2-A835-558416CE0E2B} - {B2C3D4E5-F6A7-4890-BCDE-F01234567890} = {0FE6558F-2157-47F2-A835-558416CE0E2B} - {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769} = {0FE6558F-2157-47F2-A835-558416CE0E2B} - {6A1ADA81-28E9-4A64-A32D-0755876D5EB7} = {0FE6558F-2157-47F2-A835-558416CE0E2B} - {185A8BB0-344A-4856-AEB4-213866EB2EE7} = {0FE6558F-2157-47F2-A835-558416CE0E2B} {7446D193-8636-4E58-96E4-0C8CB8790679} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284} {4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284} {E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789} {C3D4E5F6-A7B8-4901-CDEF-012345678901} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789} {A19D2776-B935-BD35-4AB1-3FCE2092805A} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789} - {3BE2E134-E773-4574-ABDD-175F00E4932E} = {0FE6558F-2157-47F2-A835-558416CE0E2B} + {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {6A1ADA81-28E9-4A64-A32D-0755876D5EB7} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {185A8BB0-344A-4856-AEB4-213866EB2EE7} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {02DE69CD-19E6-43C0-8916-DB98E5B5CA89} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {069365DB-1916-4C38-A90D-5E909BD9EDD0} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {1B66E492-1830-4229-A8EF-135714BEADA2} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012} + {9582CD83-0B49-4255-9BA6-BC045C3984AD} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012} + {CFC1AED5-72BF-4E84-92B6-65819A5AC961} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012} + {31D58517-29D8-46E9-AEAC-F43FDE540590} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012} + {92CA82EB-E558-44E7-9185-6FF8B8299C2A} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6246A67B-299E-4E64-8DBE-1A66771E7C67} diff --git a/web/web.csproj b/web/web.csproj index eea6815..2e28118 100644 --- a/web/web.csproj +++ b/web/web.csproj @@ -9,7 +9,7 @@ - - + +