diff --git a/Apis/api/Controllers/ContactController.cs b/Apis/api/Controllers/ContactController.cs
index b95e2ea..122d772 100644
--- a/Apis/api/Controllers/ContactController.cs
+++ b/Apis/api/Controllers/ContactController.cs
@@ -115,7 +115,7 @@ namespace Api.Controllers
catch (Exception ex)
{
_log.LogError(ex, "Subscription failed. ip={Ip} eMail={eMail}", userIp, req.Email);
- return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "Failed.", Code = "subscription_failed" });
+ return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "Could not process subscription.", Code = "subscription_failed" });
}
}
diff --git a/Apis/api/Controllers/FileDownloadController.cs b/Apis/api/Controllers/FileDownloadController.cs
index f12a5fb..e28223e 100644
--- a/Apis/api/Controllers/FileDownloadController.cs
+++ b/Apis/api/Controllers/FileDownloadController.cs
@@ -44,10 +44,6 @@ namespace Api.Controllers
///
/// The name of the file to download (optional - uses default from settings if not provided)
/// File stream with appropriate headers for resumable downloads
- /// Full file content
- /// Partial file content (range request)
- /// File not found
- /// Requested range not satisfiable
[HttpGet("{fileName?}")]
[SwaggerOperation(Summary = "Download file", Description = "Downloads a file with support for full and ranged (resumable) transfers.")]
[SwaggerResponse(StatusCodes.Status200OK, "Full file content returned")]
@@ -135,9 +131,7 @@ namespace Api.Controllers
}
}
- ///
- /// Handles HTTP range requests for partial content downloads and resume support.
- ///
+ // Handles HTTP range requests for partial content downloads and resume support.
private async Task HandleRangeRequest(
string filePath,
long fileLength,
@@ -194,9 +188,7 @@ namespace Api.Controllers
}
}
- ///
- /// Efficiently streams a specific byte range from source to destination.
- ///
+ // Efficiently streams a specific byte range from source to destination.
private static async Task StreamRangeAsync(Stream source, Stream destination, long bytesToRead)
{
var buffer = new byte[BufferSize];
diff --git a/Apis/cv-matcher-api/Services/CvMatcherService.cs b/Apis/cv-matcher-api/Services/CvMatcherService.cs
index 023de6d..1dd361d 100644
--- a/Apis/cv-matcher-api/Services/CvMatcherService.cs
+++ b/Apis/cv-matcher-api/Services/CvMatcherService.cs
@@ -135,13 +135,6 @@ public sealed class CvMatcherService : ICvMatcherService
result.JobUrl = job.SourceUrl;
result.Cached = false;
await _repository.SaveMatchAsync(cv.Id, job.Id, language, result, ct);
-
- //await _email.SendMatchAsync(
- // email,
- // $"MyAi.ro CV Match: {result.Score}% - {job.Title}",
- // BuildEmailBody(cv, job, result),
- // ct);
-
return result;
}
@@ -188,25 +181,4 @@ public sealed class CvMatcherService : ICvMatcherService
};
private static string Limit(string value, int max) => value.Length <= max ? value : value[..max];
-
- //private static string BuildEmailBody(RagDocumentDetails cv, RagDocumentDetails job, JobMatchResponse result) => $"""
- // CV Matcher result
-
- // CV: {cv.Title}
- // Job: {job.Title}
- // Job URL: {job.SourceUrl ?? "N/A"}
- // Score: {result.Score}%
-
- // Summary:
- // {result.Summary}
-
- // Strengths:
- // - {string.Join("\n- ", result.Strengths)}
-
- // Gaps:
- // - {string.Join("\n- ", result.Gaps)}
-
- // Recommendations:
- // - {string.Join("\n- ", result.Recommendations)}
- // """;
}
diff --git a/Apis/cv-matcher-api/Services/JobTokenService.cs b/Apis/cv-matcher-api/Services/JobTokenService.cs
index 8b1f2d8..bf4036e 100644
--- a/Apis/cv-matcher-api/Services/JobTokenService.cs
+++ b/Apis/cv-matcher-api/Services/JobTokenService.cs
@@ -92,6 +92,7 @@ public sealed class JobTokenService : IJobTokenService
.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries)
.Select(l => l.Trim())
.Where(l => l.Length > 5 && l.Length < 200)
+ // Skip lines that are purely digits, spaces, and phone/contact punctuation (phone numbers, emails, etc.)
.Where(l => !Regex.IsMatch(l, @"^[\d\s\+\-\(\)\@\.]+$"))
.Take(5)
.ToList();
diff --git a/Apis/email-api/Controllers/EmailController.cs b/Apis/email-api/Controllers/EmailController.cs
index e7e628f..6cafe16 100644
--- a/Apis/email-api/Controllers/EmailController.cs
+++ b/Apis/email-api/Controllers/EmailController.cs
@@ -5,6 +5,11 @@ using Swashbuckle.AspNetCore.Annotations;
namespace EmailApi.Controllers;
+///
+/// Internal email relay. Accepts an HTML body fragment from trusted callers
+/// (api, cv-search-job), wraps it in the branded HTML shell, and dispatches
+/// via SMTP. Protected by X-Internal-Api-Key.
+///
[ApiController]
[Route("api/email")]
public sealed class EmailController : ControllerBase
@@ -13,9 +18,27 @@ public sealed class EmailController : ControllerBase
public EmailController(SmtpEmailDispatcher dispatcher) => _dispatcher = dispatcher;
+ ///
+ /// Sends an HTML email via SMTP. The supplied body fragment is wrapped in
+ /// the branded HTML shell before dispatch. Attachments are resolved from
+ /// the shared file storage volume using the relative path in
+ /// .
+ ///
+ /// Email payload: recipients, subject, HTML body fragment, optional attachment path.
+ /// Cancellation token.
+ /// 204 No Content on success.
[HttpPost("send")]
- [SwaggerOperation(Summary = "Send an HTML email via SMTP")]
+ [SwaggerOperation(
+ Summary = "Send an HTML email via SMTP",
+ Description = "Wraps the provided HTML body in the branded shell and sends via SMTP. " +
+ "If AttachmentPath is set, resolves the file from the shared file-storage volume. " +
+ "Returns 204 on success; 400 when the request body is invalid; 500 on SMTP failure.")]
+ [SwaggerResponse(StatusCodes.Status204NoContent, "Email dispatched successfully")]
+ [SwaggerResponse(StatusCodes.Status400BadRequest, "Request body is missing or invalid")]
+ [SwaggerResponse(StatusCodes.Status500InternalServerError, "SMTP dispatch failed")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task Send([FromBody] SendEmailRequest request, CancellationToken ct)
{
await _dispatcher.SendAsync(request, ct);
diff --git a/Apis/rag-api/Services/DocumentClassifier.cs b/Apis/rag-api/Services/DocumentClassifier.cs
index ae64279..28c8b8c 100644
--- a/Apis/rag-api/Services/DocumentClassifier.cs
+++ b/Apis/rag-api/Services/DocumentClassifier.cs
@@ -24,6 +24,8 @@ public sealed class DocumentClassifier : IDocumentClassifier
});
}
+ // Keyword-frequency heuristic: count how many characteristic terms each document
+ // type contributes to the text, then pick the type with the highest hit count.
var lower = text.ToLowerInvariant();
var scores = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
@@ -37,6 +39,8 @@ public sealed class DocumentClassifier : IDocumentClassifier
var best = scores.OrderByDescending(x => x.Value).First();
var type = best.Value <= 0 ? "unknown" : best.Key;
+ // Confidence baseline 0.45 + 0.08 per matched keyword term, capped at 0.95.
+ // Zero hits → 0.25 (effectively unknown).
var confidence = best.Value <= 0 ? 0.25 : Math.Min(0.95, 0.45 + best.Value * 0.08);
return Task.FromResult(new DocumentClassification
diff --git a/Apis/rag-api/Services/TextChunker.cs b/Apis/rag-api/Services/TextChunker.cs
index 434f2b9..0b011fb 100644
--- a/Apis/rag-api/Services/TextChunker.cs
+++ b/Apis/rag-api/Services/TextChunker.cs
@@ -10,6 +10,8 @@ public sealed class TextChunker : ITextChunker
chunkSize = Math.Clamp(chunkSize, 300, 3000);
overlap = Math.Clamp(overlap, 0, chunkSize / 2);
+ // Sliding window: step forward by (chunkSize - overlap) each iteration so
+ // adjacent chunks share `overlap` characters, preserving cross-boundary context.
var chunks = new List();
var start = 0;
while (start < text.Length)
diff --git a/Jobs/cv-search-job/Services/HtmlJobSearcher.cs b/Jobs/cv-search-job/Services/HtmlJobSearcher.cs
index fe03132..7fba235 100644
--- a/Jobs/cv-search-job/Services/HtmlJobSearcher.cs
+++ b/Jobs/cv-search-job/Services/HtmlJobSearcher.cs
@@ -75,6 +75,7 @@ public sealed class HtmlJobSearcher
continue;
}
+ // Strip query string and fragment so different tracking variants of the same URL collapse to one.
var url = absoluteUri.GetLeftPart(UriPartial.Path);
if (seen.Add(url))
results.Add(url);
diff --git a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs
index 593baf7..16b0087 100644
--- a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs
+++ b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs
@@ -125,6 +125,7 @@ public sealed class CvSearchJobTask : IJobTask
{
CvDocumentId = session.CvDocumentId,
JobUrl = url,
+ // User already gave GDPR consent when they clicked the one-time job search link
GdprConsent = true
};
@@ -191,6 +192,7 @@ public sealed class CvSearchJobTask : IJobTask
private static string BuildCvFileName(string cvDocumentId)
{
+ // Strip non-alphanumeric characters so the filename is safe for all OS/email clients.
var safeId = string.Concat(cvDocumentId.Where(char.IsLetterOrDigit));
if (string.IsNullOrWhiteSpace(safeId)) safeId = "cv";
return $"{safeId}.pdf";