Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb83d28ed5 | |||
| d2b12e39ec | |||
| 1e8758796e | |||
| b6d9aea3bc | |||
| ef2793448a | |||
| 2b9132a3a9 | |||
| cbf06031e8 | |||
| 90f540139a | |||
| e5bf56cc4d | |||
| 71d5ac8e06 | |||
| c2082d6729 | |||
| 8f58708cd9 | |||
| 06dd0140d6 | |||
| 0aee7c4ed6 | |||
| cd661fe613 | |||
| 6f1d8992ab | |||
| 2d9ffc9c2b | |||
| 9fbad722fc |
@@ -3,7 +3,7 @@ name: Build and Push Docker Images Staging
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- staging
|
- production
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GIT_HOST: git.easysoft.ro
|
GIT_HOST: git.easysoft.ro
|
||||||
@@ -16,7 +16,7 @@ env:
|
|||||||
CV_CLEANUP_JOB_IMAGE: apps/myai-cv-cleanup-job
|
CV_CLEANUP_JOB_IMAGE: apps/myai-cv-cleanup-job
|
||||||
CV_SEARCH_JOB_IMAGE: apps/myai-cv-search-job
|
CV_SEARCH_JOB_IMAGE: apps/myai-cv-search-job
|
||||||
PAGE_FETCHER_API_IMAGE: apps/myai-page-fetcher-api
|
PAGE_FETCHER_API_IMAGE: apps/myai-page-fetcher-api
|
||||||
IMAGE_TAG: staging
|
IMAGE_TAG: production
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -170,11 +170,10 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
!string.IsNullOrWhiteSpace(request.JobDescription));
|
!string.IsNullOrWhiteSpace(request.JobDescription));
|
||||||
var res = await _cvApi.MatchJob(request, ct);
|
var res = await _cvApi.MatchJob(request, ct);
|
||||||
var attachmentPath = TryGetCachedCvPath(request.CvDocumentId);
|
var attachmentPath = TryGetCachedCvPath(request.CvDocumentId);
|
||||||
|
var language = NormalizeLanguage(request.Language);
|
||||||
var jobLabel = !string.IsNullOrWhiteSpace(request.JobUrl)
|
var jobLabel = !string.IsNullOrWhiteSpace(request.JobUrl)
|
||||||
? request.JobUrl
|
? request.JobUrl
|
||||||
: "Manual job description";
|
: _emailSender.GetManualJobLabel(language);
|
||||||
|
|
||||||
var language = NormalizeLanguage(request.Language);
|
|
||||||
|
|
||||||
string? jobSearchLink = null;
|
string? jobSearchLink = null;
|
||||||
if (!string.IsNullOrWhiteSpace(request.Email) && !string.IsNullOrWhiteSpace(request.CvDocumentId))
|
if (!string.IsNullOrWhiteSpace(request.Email) && !string.IsNullOrWhiteSpace(request.CvDocumentId))
|
||||||
|
|||||||
@@ -61,5 +61,11 @@ namespace Api.Services.Contracts
|
|||||||
/// <param name="expiryDays">Number of days until the job-search link expires (shown in the footer copy).</param>
|
/// <param name="expiryDays">Number of days until the job-search link expires (shown in the footer copy).</param>
|
||||||
/// <returns>Rendered HTML body string.</returns>
|
/// <returns>Rendered HTML body string.</returns>
|
||||||
string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7);
|
string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the localised label for a manually-entered job description (no URL provided).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">Two-letter language code.</param>
|
||||||
|
string GetManualJobLabel(string language);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public sealed class EmailApiEmailSender : IEmailSender
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="background:#f8f9fa">
|
<tr style="background:#f8f9fa">
|
||||||
<td style="font-weight:600;border:1px solid #dee2e6;color:#495057">IP Address</td>
|
<td style="font-weight:600;border:1px solid #dee2e6;color:#495057">IP Address</td>
|
||||||
<td style="border:1px solid #dee2e6">{userIp ?? "Unknown"}</td>
|
<td style="border:1px solid #dee2e6">{userIp ?? _emailTemplates.Get("email.notification.unknown-ip", "en")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
""";
|
""";
|
||||||
@@ -215,8 +215,8 @@ public sealed class EmailApiEmailSender : IEmailSender
|
|||||||
// email.match.body is now stored as HTML in the database
|
// email.match.body is now stored as HTML in the database
|
||||||
var body = _emailTemplates.Render("email.match.body", language,
|
var body = _emailTemplates.Render("email.match.body", language,
|
||||||
("cvDocumentId", cvDocumentId),
|
("cvDocumentId", cvDocumentId),
|
||||||
("jobLabel", jobLabel ?? "N/A"),
|
("jobLabel", jobLabel ?? _emailTemplates.Get("email.match.fallback-na", language)),
|
||||||
("jobUrl", result.JobUrl ?? "N/A"),
|
("jobUrl", result.JobUrl ?? _emailTemplates.Get("email.match.fallback-na", language)),
|
||||||
("score", result.Score.ToString()),
|
("score", result.Score.ToString()),
|
||||||
("summary", WebUtility.HtmlEncode(result.Summary ?? string.Empty)),
|
("summary", WebUtility.HtmlEncode(result.Summary ?? string.Empty)),
|
||||||
("strengths", strengths),
|
("strengths", strengths),
|
||||||
@@ -238,5 +238,8 @@ public sealed class EmailApiEmailSender : IEmailSender
|
|||||||
public string BuildMatchEmailSubject(int score, string? jobLabel, string language) =>
|
public string BuildMatchEmailSubject(int score, string? jobLabel, string language) =>
|
||||||
_emailTemplates.Render("email.match.subject", language,
|
_emailTemplates.Render("email.match.subject", language,
|
||||||
("score", score.ToString()),
|
("score", score.ToString()),
|
||||||
("jobLabel", jobLabel ?? "Job"));
|
("jobLabel", jobLabel ?? _emailTemplates.Get("email.match.subject-fallback-label", language)));
|
||||||
|
|
||||||
|
public string GetManualJobLabel(string language) =>
|
||||||
|
_emailTemplates.Get("email.match.manual-job-label", language);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
var json = await _ai.CreateChatCompletionAsync(systemPrompt, userPrompt, 0.2m, ct);
|
var json = await _ai.CreateChatCompletionAsync(systemPrompt, userPrompt, 0.2m, ct);
|
||||||
var result = ParseResult(json);
|
var errorSummary = await _aiPrompts.GetAsync("parse-error.summary", language, ct);
|
||||||
|
var errorRec = await _aiPrompts.GetAsync("parse-error.recommendation", language, ct);
|
||||||
|
var result = ParseResult(json, errorSummary, errorRec);
|
||||||
result.JobDocumentId = job.Id;
|
result.JobDocumentId = job.Id;
|
||||||
result.JobUrl = job.SourceUrl;
|
result.JobUrl = job.SourceUrl;
|
||||||
result.Cached = false;
|
result.Cached = false;
|
||||||
@@ -153,7 +155,10 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
/// Deserialises the LLM's JSON output into a <see cref="JobMatchResponse"/>.
|
/// Deserialises the LLM's JSON output into a <see cref="JobMatchResponse"/>.
|
||||||
/// Returns a safe fallback response instead of throwing when the JSON cannot be parsed.
|
/// Returns a safe fallback response instead of throwing when the JSON cannot be parsed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static JobMatchResponse ParseResult(string json)
|
private static JobMatchResponse ParseResult(
|
||||||
|
string json,
|
||||||
|
string? errorSummary = null,
|
||||||
|
string? errorRec = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -168,8 +173,8 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
return new JobMatchResponse
|
return new JobMatchResponse
|
||||||
{
|
{
|
||||||
Score = 0,
|
Score = 0,
|
||||||
Summary = "The AI response could not be parsed as structured JSON.",
|
Summary = errorSummary ?? "The AI response could not be parsed as structured JSON.",
|
||||||
Recommendations = ["Inspect the raw model output and tune the scoring prompt."]
|
Recommendations = [errorRec ?? "Inspect the raw model output and tune the scoring prompt."]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CvMatcher.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CvMatcher.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CvMatcherDbContext))]
|
||||||
|
[Migration("20260608193046_AddParseErrorPrompts")]
|
||||||
|
partial class AddParseErrorPrompts
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("cvMatcher")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.AiPromptEntity", 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<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Language");
|
||||||
|
|
||||||
|
b.ToTable("AiPrompts", "cvMatcher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("ClientIpAddress")
|
||||||
|
.HasMaxLength(45)
|
||||||
|
.HasColumnType("nvarchar(45)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("CvDocumentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("JobDocumentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ResultJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("Score")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CvDocumentId", "JobDocumentId", "Language")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Results", "cvMatcher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("CacheKey")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("ResponseText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Temperature")
|
||||||
|
.HasColumnType("decimal(4,2)");
|
||||||
|
|
||||||
|
b.HasKey("CacheKey");
|
||||||
|
|
||||||
|
b.ToTable("ChatCache", "cvMatcher");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using CvMatcher.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CvMatcher.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddParseErrorPrompts : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["parse-error.summary", "en", "The AI response could not be parsed. Please try again.", "Summary shown in match email when the AI returns an unparseable response"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["parse-error.summary", "ro", "Răspunsul AI nu a putut fi interpretat. Vă rugăm să încercați din nou.", "Sumar afișat în emailul de potrivire când AI returnează un răspuns care nu poate fi interpretat"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["parse-error.recommendation", "en", "If the problem persists, try a different job link or description.", "Recommendation shown in match email when the AI returns an unparseable response"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["parse-error.recommendation", "ro", "Dacă problema persistă, încercați un alt link sau descriere de job.", "Recomandare afișată în emailul de potrivire când AI returnează un răspuns care nu poate fi interpretat"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["parse-error.summary", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["parse-error.summary", "ro"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["parse-error.recommendation", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "AiPrompts",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["parse-error.recommendation", "ro"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ public sealed class SmtpEmailDispatcher
|
|||||||
if (!string.IsNullOrWhiteSpace(req.ReplyTo))
|
if (!string.IsNullOrWhiteSpace(req.ReplyTo))
|
||||||
msg.ReplyTo.Add(MailboxAddress.Parse(req.ReplyTo));
|
msg.ReplyTo.Add(MailboxAddress.Parse(req.ReplyTo));
|
||||||
|
|
||||||
msg.Subject = $"[{_environmentName}] {req.Subject}".Trim();
|
msg.Subject = req.Subject.Trim();
|
||||||
|
|
||||||
var shellStart = _templates.Get("email.html-shell.start", "*");
|
var shellStart = _templates.Get("email.html-shell.start", "*");
|
||||||
var shellEnd = _templates.Get("email.html-shell.end", "*");
|
var shellEnd = _templates.Get("email.html-shell.end", "*");
|
||||||
|
|||||||
+69
@@ -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("20260608190611_AddManualJobLabelTemplate")]
|
||||||
|
partial class AddManualJobLabelTemplate
|
||||||
|
{
|
||||||
|
/// <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,43 @@
|
|||||||
|
using Email.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Email.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddManualJobLabelTemplate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.match.manual-job-label", "en", "Manual job description", "Label used in the match email subject and body when no job URL was provided"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.match.manual-job-label", "ro", "Descriere manuală a jobului", "Etichetă folosită în subiectul și corpul emailului când nu a fost furnizat un URL de job"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.match.manual-job-label", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.match.manual-job-label", "ro"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -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("20260608192938_AddFallbackStringTemplates")]
|
||||||
|
partial class AddFallbackStringTemplates
|
||||||
|
{
|
||||||
|
/// <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,163 @@
|
|||||||
|
using Email.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Email.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFallbackStringTemplates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.match.fallback-na", "en", "N/A", "Fallback when a match email field (job label or URL) has no value"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.match.fallback-na", "ro", "N/A", "Fallback când un câmp al emailului de potrivire (etichetă job sau URL) nu are valoare"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.match.subject-fallback-label", "en", "Job", "Fallback job label used in match email subject when no specific label is available"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.match.subject-fallback-label", "ro", "Job", "Etichetă fallback pentru subiectul emailului de potrivire când nu există o etichetă specifică"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.notification.unknown-ip", "en", "Unknown", "Fallback IP address label in operator notification emails"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.notification.unknown-ip", "ro", "Necunoscut", "Etichetă fallback pentru adresa IP în emailurile de notificare operator"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.search-results.keywords-empty", "en", "none detected", "Text shown in job search results email when no CV keywords were extracted"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.search-results.keywords-empty", "ro", "niciunul detectat", "Text afișat în emailul cu rezultatele căutării când nu au fost extrase cuvinte cheie din CV"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.search-results.providers-empty", "en", "none", "Text shown in job search results email when no providers were searched"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.search-results.providers-empty", "ro", "niciunul", "Text afișat în emailul cu rezultatele căutării când nu au fost căutați furnizori"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.search-results.location-empty", "en", "-", "Fallback location display in job search results email scan summary"]);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: ["email.search-results.location-empty", "ro", "-", "Afișaj fallback pentru locație în sumarului de scanare al emailului cu rezultatele căutării"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.match.fallback-na", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.match.fallback-na", "ro"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.match.subject-fallback-label", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.match.subject-fallback-label", "ro"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.notification.unknown-ip", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.notification.unknown-ip", "ro"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.keywords-empty", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.keywords-empty", "ro"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.providers-empty", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.providers-empty", "ro"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.location-empty", "en"]);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.location-empty", "ro"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,4 +17,10 @@ public sealed class FetchPageRequest
|
|||||||
/// Identifies the calling service for audit purposes (e.g. <c>cv-matcher-api</c>, <c>cv-search-job</c>).
|
/// Identifies the calling service for audit purposes (e.g. <c>cv-matcher-api</c>, <c>cv-search-job</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CallerService { get; set; } = string.Empty;
|
public string CallerService { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional reference to the job search session that triggered this fetch.
|
||||||
|
/// Stored on <c>pageFetcher.PageFetches</c> for cross-schema audit queries.
|
||||||
|
/// </summary>
|
||||||
|
public string? JobSearchSessionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ public sealed class PageFetcherService
|
|||||||
Id = Guid.NewGuid().ToString("N"),
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
Url = request.Url,
|
Url = request.Url,
|
||||||
CallerService = request.CallerService ?? string.Empty,
|
CallerService = request.CallerService ?? string.Empty,
|
||||||
|
JobSearchSessionId = request.JobSearchSessionId,
|
||||||
HttpStatusCode = statusCode,
|
HttpStatusCode = statusCode,
|
||||||
Html = html,
|
Html = html,
|
||||||
Text = text,
|
Text = text,
|
||||||
|
|||||||
@@ -31,4 +31,10 @@ public sealed class PageFetchEntity : BaseEntity
|
|||||||
|
|
||||||
/// <summary>Exception message when <see cref="Success"/> is <c>false</c>.</summary>
|
/// <summary>Exception message when <see cref="Success"/> is <c>false</c>.</summary>
|
||||||
public string? ErrorMessage { get; set; }
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional reference to the <c>cvSearch.JobSearchSessions</c> row that triggered this fetch.
|
||||||
|
/// Null for fetches not originating from a job search session (e.g. direct CV-to-job matches).
|
||||||
|
/// </summary>
|
||||||
|
public string? JobSearchSessionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PageFetcher.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PageFetcher.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PageFetchDbContext))]
|
||||||
|
[Migration("20260608165542_AddJobSearchSessionId")]
|
||||||
|
partial class AddJobSearchSessionId
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("pageFetcher")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("PageFetcher.Data.Entities.PageFetchEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("CallerService")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<long>("DurationMs")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("Html")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int?>("HttpStatusCode")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("JobSearchSessionId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<bool>("Success")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("JobSearchSessionId");
|
||||||
|
|
||||||
|
b.HasIndex("Url");
|
||||||
|
|
||||||
|
b.ToTable("PageFetches", "pageFetcher");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using PageFetcher.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PageFetcher.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobSearchSessionId : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "JobSearchSessionId",
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "PageFetches",
|
||||||
|
type: "nvarchar(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PageFetches_JobSearchSessionId",
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "PageFetches",
|
||||||
|
column: "JobSearchSessionId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_PageFetches_JobSearchSessionId",
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "PageFetches");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "JobSearchSessionId",
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "PageFetches");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,10 @@ namespace PageFetcher.Data.Migrations
|
|||||||
b.Property<int?>("HttpStatusCode")
|
b.Property<int?>("HttpStatusCode")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("JobSearchSessionId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
b.Property<bool>("Success")
|
b.Property<bool>("Success")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
@@ -69,6 +73,8 @@ namespace PageFetcher.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("JobSearchSessionId");
|
||||||
|
|
||||||
b.HasIndex("Url");
|
b.HasIndex("Url");
|
||||||
|
|
||||||
b.ToTable("PageFetches", "pageFetcher");
|
b.ToTable("PageFetches", "pageFetcher");
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ public sealed class PageFetchDbContext : DbContext
|
|||||||
entity.Property(x => x.Html).IsRequired();
|
entity.Property(x => x.Html).IsRequired();
|
||||||
entity.Property(x => x.Text).IsRequired();
|
entity.Property(x => x.Text).IsRequired();
|
||||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||||
|
entity.Property(x => x.JobSearchSessionId).HasMaxLength(64);
|
||||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
entity.HasIndex(x => x.JobSearchSessionId);
|
||||||
|
|
||||||
entity.HasIndex(x => x.Url);
|
entity.HasIndex(x => x.Url);
|
||||||
entity.HasIndex(x => x.CreatedAt);
|
entity.HasIndex(x => x.CreatedAt);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -127,13 +127,15 @@ public sealed class CvSearchEmailSender
|
|||||||
var keywordsHtml = keywords.Count > 0
|
var keywordsHtml = keywords.Count > 0
|
||||||
? string.Join(" ", keywords.Select(k =>
|
? string.Join(" ", keywords.Select(k =>
|
||||||
$"<span style=\"display:inline-block;background:#e9ecef;padding:2px 8px;margin:2px 2px 2px 0;font-size:12px;\">{k}</span>"))
|
$"<span style=\"display:inline-block;background:#e9ecef;padding:2px 8px;margin:2px 2px 2px 0;font-size:12px;\">{k}</span>"))
|
||||||
: "<span style=\"color:#6c757d;font-size:12px;font-style:italic;\">none detected</span>";
|
: $"<span style=\"color:#6c757d;font-size:12px;font-style:italic;\">{_emailTemplates.Get("email.search-results.keywords-empty", language)}</span>";
|
||||||
|
|
||||||
var providers = providerNames.Count > 0
|
var providers = providerNames.Count > 0
|
||||||
? string.Join(", ", providerNames)
|
? string.Join(", ", providerNames)
|
||||||
: "none";
|
: _emailTemplates.Get("email.search-results.providers-empty", language);
|
||||||
|
|
||||||
var locationDisplay = string.IsNullOrWhiteSpace(location) ? "-" : location;
|
var locationDisplay = string.IsNullOrWhiteSpace(location)
|
||||||
|
? _emailTemplates.Get("email.search-results.location-empty", language)
|
||||||
|
: location;
|
||||||
|
|
||||||
return _emailTemplates.Render("email.search-results.scan-summary", language,
|
return _emailTemplates.Render("email.search-results.scan-summary", language,
|
||||||
("keywordsHtml", keywordsHtml),
|
("keywordsHtml", keywordsHtml),
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ public sealed class CvSearchJobTask : IJobTask
|
|||||||
{
|
{
|
||||||
Url = url,
|
Url = url,
|
||||||
WaitFor = "domcontentloaded",
|
WaitFor = "domcontentloaded",
|
||||||
CallerService = "cv-search-job"
|
CallerService = "cv-search-job",
|
||||||
|
JobSearchSessionId = session.Id
|
||||||
}, ct);
|
}, ct);
|
||||||
|
|
||||||
if (!fetchResponse.Success || string.IsNullOrWhiteSpace(fetchResponse.Text))
|
if (!fetchResponse.Success || string.IsNullOrWhiteSpace(fetchResponse.Text))
|
||||||
@@ -197,7 +198,9 @@ public sealed class CvSearchJobTask : IJobTask
|
|||||||
// Pre-fetched text passed directly so cv-matcher-api skips re-fetching the page
|
// Pre-fetched text passed directly so cv-matcher-api skips re-fetching the page
|
||||||
JobDescription = jobText,
|
JobDescription = jobText,
|
||||||
// User already gave GDPR consent when they clicked the one-time job search link
|
// User already gave GDPR consent when they clicked the one-time job search link
|
||||||
GdprConsent = true
|
GdprConsent = true,
|
||||||
|
// Propagate language so the LLM uses the correct language-specific prompt
|
||||||
|
Language = session.Language
|
||||||
};
|
};
|
||||||
|
|
||||||
var matchResult = await _matcherApi.MatchJobAsync(matchRequest, ct);
|
var matchResult = await _matcherApi.MatchJobAsync(matchRequest, ct);
|
||||||
|
|||||||
@@ -115,7 +115,10 @@
|
|||||||
"cv.noItems": "No items yet.",
|
"cv.noItems": "No items yet.",
|
||||||
"cv.strengths": "Strengths",
|
"cv.strengths": "Strengths",
|
||||||
"cv.gaps": "Gaps",
|
"cv.gaps": "Gaps",
|
||||||
"cv.evidence": "Supporting CV excerpts"
|
"cv.evidence": "Supporting CV excerpts",
|
||||||
|
"error.cv_file_missing": "Missing CV PDF.",
|
||||||
|
"error.captcha_verification_failed": "Captcha verification failed.",
|
||||||
|
"error.request_cancelled": "Request was cancelled."
|
||||||
},
|
},
|
||||||
ro: {
|
ro: {
|
||||||
"brand.subtitle": "prezentare inginerie AI",
|
"brand.subtitle": "prezentare inginerie AI",
|
||||||
@@ -224,7 +227,10 @@
|
|||||||
"cv.noItems": "Niciun element.",
|
"cv.noItems": "Niciun element.",
|
||||||
"cv.strengths": "Puncte forte",
|
"cv.strengths": "Puncte forte",
|
||||||
"cv.gaps": "Lipsuri",
|
"cv.gaps": "Lipsuri",
|
||||||
"cv.evidence": "Fragmente relevante din CV"
|
"cv.evidence": "Fragmente relevante din CV",
|
||||||
|
"error.cv_file_missing": "Fișierul CV PDF lipsește.",
|
||||||
|
"error.captcha_verification_failed": "Verificarea captcha a eșuat.",
|
||||||
|
"error.request_cancelled": "Cererea a fost anulată."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function isValidEmail(value) {
|
|||||||
*
|
*
|
||||||
* Rules:
|
* Rules:
|
||||||
* - 429 (rate limit) → return rateLimitKey translation
|
* - 429 (rate limit) → return rateLimitKey translation
|
||||||
|
* - 4xx with known error code → look up 'error.<code>' in i18n dictionary first
|
||||||
* - 4xx with error body → return server's error message (intentional feedback)
|
* - 4xx with error body → return server's error message (intentional feedback)
|
||||||
* - 5xx or no body → return fallbackKey translation
|
* - 5xx or no body → return fallbackKey translation
|
||||||
*
|
*
|
||||||
@@ -65,8 +66,17 @@ function extractApiError(body, status, fallbackKey, rateLimitKey) {
|
|||||||
if (status === 429) {
|
if (status === 429) {
|
||||||
return window.MyAi.t(rateLimitKey || 'form.rateLimited');
|
return window.MyAi.t(rateLimitKey || 'form.rateLimited');
|
||||||
}
|
}
|
||||||
var msg = body && (body.error || body.Error || body.title);
|
if (status >= 400 && status < 500) {
|
||||||
return (status >= 400 && status < 500 && msg) ? msg : window.MyAi.t(fallbackKey);
|
// Prefer i18n translation keyed on the machine-readable error code
|
||||||
|
if (body && body.code) {
|
||||||
|
var codeKey = 'error.' + body.code;
|
||||||
|
var translated = window.MyAi.t(codeKey);
|
||||||
|
if (translated !== codeKey) return translated;
|
||||||
|
}
|
||||||
|
var msg = body && (body.error || body.Error || body.title);
|
||||||
|
if (msg) return msg;
|
||||||
|
}
|
||||||
|
return window.MyAi.t(fallbackKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose helpers on window.MyAi for use by other scripts
|
// Expose helpers on window.MyAi for use by other scripts
|
||||||
|
|||||||
Reference in New Issue
Block a user