Create separate migration for HTML shell templates
Build and Push Docker Images Staging / build (push) Successful in 34s

- Remove html-shell entries from InitialSchema Seed() method
- Create new AddHtmlShellTemplates migration to insert html-shell templates
- Prevents duplicate key errors from having same data in two migrations
- InitialSchema seeds 32 templates (16 keys × 2 languages)
- AddHtmlShellTemplates seeds 2 html-shell templates (start, end)
- Total: 34 templates after both migrations run

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:55:10 +03:00
parent d4c05d7d44
commit 9cb38e5bc8
4 changed files with 140 additions and 55 deletions
@@ -1,4 +1,5 @@
using System;
using CvMatcher.Data;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -12,11 +13,11 @@ namespace CvMatcher.Data.Migrations
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "cvMatcher");
name: MigrationConstants.SchemaName);
migrationBuilder.CreateTable(
name: "AiPrompts",
schema: "cvMatcher",
schema: MigrationConstants.SchemaName,
columns: table => new
{
Key = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
@@ -32,7 +33,7 @@ namespace CvMatcher.Data.Migrations
migrationBuilder.CreateTable(
name: "ChatCache",
schema: "cvMatcher",
schema: MigrationConstants.SchemaName,
columns: table => new
{
CacheKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
@@ -48,7 +49,7 @@ namespace CvMatcher.Data.Migrations
migrationBuilder.CreateTable(
name: "Results",
schema: "cvMatcher",
schema: MigrationConstants.SchemaName,
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
@@ -66,19 +67,28 @@ namespace CvMatcher.Data.Migrations
migrationBuilder.CreateIndex(
name: "IX_Results_CvDocumentId_JobDocumentId_Language",
schema: "cvMatcher",
schema: MigrationConstants.SchemaName,
table: "Results",
columns: new[] { "CvDocumentId", "JobDocumentId", "Language" },
unique: true);
// Seed AI prompts for CV matching (language-specific)
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.
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ă.
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.');
");
Seed(migrationBuilder);
}
private static void Seed(MigrationBuilder m)
{
void Row(string key, string lang, string value, string description = "")
=> 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 />
@@ -86,15 +96,15 @@ JSON shape: {""score"":number,""summary"":""rezumat pe o linie în română"",""
{
migrationBuilder.DropTable(
name: "AiPrompts",
schema: "cvMatcher");
schema: MigrationConstants.SchemaName);
migrationBuilder.DropTable(
name: "ChatCache",
schema: "cvMatcher");
schema: MigrationConstants.SchemaName);
migrationBuilder.DropTable(
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.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");
// 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 />
@@ -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);
}
}
}