Staging to Production #51

Merged
claude merged 165 commits from main into production 2026-06-08 18:28:46 +00:00
4 changed files with 140 additions and 55 deletions
Showing only changes of commit 9cb38e5bc8 - Show all commits
@@ -1,4 +1,5 @@
using System; using System;
using CvMatcher.Data;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
@@ -12,11 +13,11 @@ namespace CvMatcher.Data.Migrations
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.EnsureSchema( migrationBuilder.EnsureSchema(
name: "cvMatcher"); name: MigrationConstants.SchemaName);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AiPrompts", name: "AiPrompts",
schema: "cvMatcher", schema: MigrationConstants.SchemaName,
columns: table => new columns: table => new
{ {
Key = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false), Key = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
@@ -32,7 +33,7 @@ namespace CvMatcher.Data.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ChatCache", name: "ChatCache",
schema: "cvMatcher", schema: MigrationConstants.SchemaName,
columns: table => new columns: table => new
{ {
CacheKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false), CacheKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
@@ -48,7 +49,7 @@ namespace CvMatcher.Data.Migrations
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Results", name: "Results",
schema: "cvMatcher", schema: MigrationConstants.SchemaName,
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false), Id = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
@@ -66,19 +67,28 @@ namespace CvMatcher.Data.Migrations
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Results_CvDocumentId_JobDocumentId_Language", name: "IX_Results_CvDocumentId_JobDocumentId_Language",
schema: "cvMatcher", schema: MigrationConstants.SchemaName,
table: "Results", table: "Results",
columns: new[] { "CvDocumentId", "JobDocumentId", "Language" }, columns: new[] { "CvDocumentId", "JobDocumentId", "Language" },
unique: true); unique: true);
// Seed AI prompts for CV matching (language-specific) Seed(migrationBuilder);
migrationBuilder.Sql(@" }
INSERT INTO [cvMatcher].AiPrompts ([Key], [Language], [Value], [Description]) VALUES
('ai.cv-match.system-prompt', 'en', 'You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100. Penalize missing required skills. Do not invent experience. Use concise business language. All text fields in the JSON response must be in English. private static void Seed(MigrationBuilder m)
JSON shape: {""score"":number,""summary"":""one-line summary in English"",""strengths"":[""strength 1 in English"",""strength 2 in English""],""gaps"":[""gap 1 in English""],""recommendations"":[""recommendation 1 in English""],""evidence"":[""evidence 1 in English""]}', 'System prompt for CV-to-job matching in English. Instructs LLM to return JSON with CV strengths, gaps, and recommendations relative to the job.'), {
('ai.cv-match.system-prompt', 'ro', 'Ești un motor strict de potrivire CV-job. Returnează doar JSON. Punctează realist între 0 și 100. Penalizează abilitățile lipsă necesare. Nu inventa experiență. Folosește limbaj profesional concis. Toate câmpurile text din răspunsul JSON trebuie să fie în limba română. void Row(string key, string lang, string value, string description = "")
JSON shape: {""score"":number,""summary"":""rezumat pe o linie în română"",""strengths"":[""punct forte 1 în română"",""punct forte 2 în română""],""gaps"":[""lipsă 1 în română""],""recommendations"":[""recomandare 1 în română""],""evidence"":[""dovadă 1 în română""]}', 'System prompt pentru potrivire CV-job în limba română. Instruiește LLM-ul să returneze JSON cu punctele forte ale CV-ului, lacunele și recomandări relative la job.'); => m.InsertData("AiPrompts", ["Key", "Language", "Value", "Description"], [key, lang, value, description], MigrationConstants.SchemaName);
");
// AI system prompt for CV matching — English
Row("ai.cv-match.system-prompt", "en",
"You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100. Penalize missing required skills. Do not invent experience. Use concise business language. All text fields in the JSON response must be in English.\nJSON shape: {\"score\":number,\"summary\":\"one-line summary in English\",\"strengths\":[\"strength 1 in English\",\"strength 2 in English\"],\"gaps\":[\"gap 1 in English\"],\"recommendations\":[\"recommendation 1 in English\"],\"evidence\":[\"evidence 1 in English\"]}",
"System prompt for CV-to-job matching in English. Instructs LLM to return JSON with CV strengths, gaps, and recommendations relative to the job.");
// AI system prompt for CV matching — Romanian
Row("ai.cv-match.system-prompt", "ro",
"Ești un motor strict de potrivire CV-job. Returnează doar JSON. Punctează realist între 0 și 100. Penalizează abilitățile lipsă necesare. Nu inventa experiență. Folosește limbaj profesional concis. Toate câmpurile text din răspunsul JSON trebuie să fie în limba română.\nJSON shape: {\"score\":number,\"summary\":\"rezumat pe o linie în română\",\"strengths\":[\"punct forte 1 în română\",\"punct forte 2 în română\"],\"gaps\":[\"lipsă 1 în română\"],\"recommendations\":[\"recomandare 1 în română\"],\"evidence\":[\"dovadă 1 în română\"]}",
"System prompt pentru potrivire CV-job în limba română. Instruiește LLM-ul să returneze JSON cu punctele forte ale CV-ului, lacunele și recomandări relative la job.");
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -86,15 +96,15 @@ JSON shape: {""score"":number,""summary"":""rezumat pe o linie în română"",""
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AiPrompts", name: "AiPrompts",
schema: "cvMatcher"); schema: MigrationConstants.SchemaName);
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ChatCache", name: "ChatCache",
schema: "cvMatcher"); schema: MigrationConstants.SchemaName);
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Results", name: "Results",
schema: "cvMatcher"); schema: MigrationConstants.SchemaName);
} }
} }
} }
@@ -97,45 +97,6 @@ namespace Email.Data.Migrations
Row("html.job-search.error.message", "en", "An error occurred. Please try again later.", "Message 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.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"); 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");
// HTML email shell — opening tags (blue header, white card container)
Row("email.html-shell.start", "*",
$$"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
.email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; }
.email-header { background-color: #2c5282; color: white; padding: 24px; text-align: center; }
.email-header h1 { margin: 0; font-size: 24px; font-weight: 600; }
.email-body { padding: 24px; }
.email-footer { background-color: #f8f9fa; padding: 16px; text-align: center; color: #6c757d; font-size: 12px; border-top: 1px solid #dee2e6; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>MyAi.ro</h1>
</div>
<div class="email-body">
""",
"Opening HTML wrapper for branded emails (blue header, white content area)");
// HTML email shell — closing tags (footer)
Row("email.html-shell.end", "*",
$$"""
</div>
<div class="email-footer">
<p>© 2026 MyAi.ro. All rights reserved.</p>
</div>
</div>
</body>
</html>
""",
"Closing HTML wrapper for branded emails (footer and closing tags)");
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -0,0 +1,69 @@
// <auto-generated />
using System;
using Email.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Email.Data.Migrations
{
[DbContext(typeof(EmailDbContext))]
[Migration("20260601145256_AddHtmlShellTemplates")]
partial class AddHtmlShellTemplates
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("email")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Email.Data.Entities.EmailTemplateEntity", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Language")
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<string>("Description")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)")
.HasDefaultValue("");
b.Property<string>("OperatorCopy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)")
.HasDefaultValue("");
b.Property<DateTime>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Key", "Language");
b.ToTable("Templates", "email");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Email.Data;
#nullable disable
namespace Email.Data.Migrations
{
/// <inheritdoc />
public partial class AddHtmlShellTemplates : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// HTML email shell — opening tags (blue header, white card container)
migrationBuilder.InsertData(
table: "Templates",
columns: new[] { "Key", "Language", "Value", "Description" },
values: new object[] { "email.html-shell.start", "*", "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <style>\n body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }\n .email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; }\n .email-header { background-color: #2c5282; color: white; padding: 24px; text-align: center; }\n .email-header h1 { margin: 0; font-size: 24px; font-weight: 600; }\n .email-body { padding: 24px; }\n .email-footer { background-color: #f8f9fa; padding: 16px; text-align: center; color: #6c757d; font-size: 12px; border-top: 1px solid #dee2e6; }\n </style>\n</head>\n<body>\n <div class=\"email-container\">\n <div class=\"email-header\">\n <h1>MyAi.ro</h1>\n </div>\n <div class=\"email-body\">\n", "Opening HTML wrapper for branded emails (blue header, white content area)" },
schema: MigrationConstants.SchemaName);
// HTML email shell — closing tags (footer)
migrationBuilder.InsertData(
table: "Templates",
columns: new[] { "Key", "Language", "Value", "Description" },
values: new object[] { "email.html-shell.end", "*", " </div>\n <div class=\"email-footer\">\n <p>© 2026 MyAi.ro. All rights reserved.</p>\n </div>\n </div>\n</body>\n</html>\n", "Closing HTML wrapper for branded emails (footer and closing tags)" },
schema: MigrationConstants.SchemaName);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "Templates",
keyColumns: new[] { "Key", "Language" },
keyValues: new object[] { "email.html-shell.start", "*" },
schema: MigrationConstants.SchemaName);
migrationBuilder.DeleteData(
table: "Templates",
keyColumns: new[] { "Key", "Language" },
keyValues: new object[] { "email.html-shell.end", "*" },
schema: MigrationConstants.SchemaName);
}
}
}