fix: close release preflight gaps
Route public health checks to the API, backfill and synchronize job opportunities, stabilize SPA smoke tests, and document operator-only production steps.
This commit is contained in:
@@ -567,6 +567,7 @@ public sealed class GmailController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(company.RecruiterEmail) && !string.IsNullOrWhiteSpace(request.RecruiterEmail)) company.RecruiterEmail = request.RecruiterEmail.Trim();
|
||||
}
|
||||
|
||||
var savedAt = DateTime.UtcNow;
|
||||
var job = new JobApplication
|
||||
{
|
||||
OwnerUserId = ownerUserId,
|
||||
@@ -574,8 +575,10 @@ public sealed class GmailController : ControllerBase
|
||||
JobTitle = jobTitle,
|
||||
Status = string.IsNullOrWhiteSpace(request.Status) ? "Applied" : request.Status.Trim(),
|
||||
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(),
|
||||
DateApplied = DateTime.UtcNow,
|
||||
DateApplied = savedAt,
|
||||
SavedAt = savedAt,
|
||||
};
|
||||
job.Job = JobOpportunitySync.Create(job, "gmail");
|
||||
_db.JobApplications.Add(job);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -702,27 +702,6 @@ Canonical profile:
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
private static void SyncOpportunity(JobApplication application, Job opportunity)
|
||||
{
|
||||
opportunity.OwnerUserId = application.OwnerUserId;
|
||||
opportunity.CompanyId = application.CompanyId;
|
||||
opportunity.JobTitle = application.JobTitle;
|
||||
opportunity.Location = application.Location;
|
||||
opportunity.JobUrl = application.JobUrl;
|
||||
opportunity.Description = application.Description;
|
||||
opportunity.TranslatedDescription = application.TranslatedDescription;
|
||||
opportunity.DescriptionLanguage = application.DescriptionLanguage;
|
||||
opportunity.ShortSummary = application.ShortSummary;
|
||||
opportunity.Tags = application.Tags;
|
||||
opportunity.Deadline = application.Deadline;
|
||||
opportunity.Salary = application.Salary;
|
||||
opportunity.SalaryMin = application.SalaryMin;
|
||||
opportunity.SalaryMax = application.SalaryMax;
|
||||
opportunity.SalaryCurrency = application.SalaryCurrency;
|
||||
opportunity.SalaryPeriod = application.SalaryPeriod;
|
||||
opportunity.SavedAt = application.SavedAt;
|
||||
}
|
||||
|
||||
private static (decimal? Min, decimal? Max, string? Currency, string? Period) NormalizeSalary(
|
||||
decimal? min, decimal? max, string? currency, string? period)
|
||||
{
|
||||
@@ -782,7 +761,7 @@ Canonical profile:
|
||||
if (source?.Length > 32) source = source[..32];
|
||||
var countryCode = string.IsNullOrWhiteSpace(request.CountryCode) ? null : request.CountryCode.Trim().ToUpperInvariant();
|
||||
if (countryCode?.Length != 2) countryCode = null;
|
||||
job.Job = new Job { Source = source, CountryCode = countryCode };
|
||||
job.Job = JobOpportunitySync.Create(job, source, countryCode);
|
||||
|
||||
// A job created straight into a pre-application stage has not been applied to, so it
|
||||
// must not carry an applied date. SyncAppliedDate also covers the reverse: a create
|
||||
@@ -803,7 +782,7 @@ Canonical profile:
|
||||
// ignore summarizer failures at create time
|
||||
}
|
||||
|
||||
SyncOpportunity(job, job.Job);
|
||||
JobOpportunitySync.Apply(job, job.Job);
|
||||
_db.JobApplications.Add(job);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -879,7 +858,7 @@ Canonical profile:
|
||||
// Records StatusChanged plus any lifecycle event the transition implies.
|
||||
JobLifecycleEvents.RecordStatusChange(_db, job, oldStatus, request.StatusChangedAt ?? DateTime.Now);
|
||||
|
||||
if (job.Job is not null) SyncOpportunity(job, job.Job);
|
||||
if (job.Job is not null) JobOpportunitySync.Apply(job, job.Job);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
public static class JobOpportunitySync
|
||||
{
|
||||
public static Job Create(JobApplication application, string? source = null, string? countryCode = null)
|
||||
{
|
||||
var opportunity = new Job { Source = source, CountryCode = countryCode };
|
||||
Apply(application, opportunity);
|
||||
return opportunity;
|
||||
}
|
||||
|
||||
public static void Apply(JobApplication application, Job opportunity)
|
||||
{
|
||||
opportunity.OwnerUserId = application.OwnerUserId;
|
||||
opportunity.CompanyId = application.CompanyId;
|
||||
opportunity.JobTitle = application.JobTitle;
|
||||
opportunity.Location = application.Location;
|
||||
opportunity.JobUrl = application.JobUrl;
|
||||
opportunity.Description = application.Description;
|
||||
opportunity.TranslatedDescription = application.TranslatedDescription;
|
||||
opportunity.DescriptionLanguage = application.DescriptionLanguage;
|
||||
opportunity.ShortSummary = application.ShortSummary;
|
||||
opportunity.Tags = application.Tags;
|
||||
opportunity.Deadline = application.Deadline;
|
||||
opportunity.Salary = application.Salary;
|
||||
opportunity.SalaryMin = application.SalaryMin;
|
||||
opportunity.SalaryMax = application.SalaryMax;
|
||||
opportunity.SalaryCurrency = application.SalaryCurrency;
|
||||
opportunity.SalaryPeriod = application.SalaryPeriod;
|
||||
opportunity.SavedAt = application.SavedAt;
|
||||
}
|
||||
|
||||
public static async Task<int> BackfillLegacyAsync(JobTrackerContext db, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var applications = await db.JobApplications
|
||||
.IgnoreQueryFilters()
|
||||
.Where(application => application.JobId == null)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var application in applications)
|
||||
application.Job = Create(application, "legacy");
|
||||
|
||||
if (applications.Count > 0)
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return applications.Count;
|
||||
}
|
||||
}
|
||||
@@ -2111,8 +2111,9 @@ public static class StartupInitializationExtensions
|
||||
|
||||
var companyOwnershipExists = ColumnExists(conn, provider, "Companies", "OwnerUserId");
|
||||
var jobOwnershipExists = ColumnExists(conn, provider, "JobApplications", "OwnerUserId");
|
||||
var opportunityOwnershipExists = ColumnExists(conn, provider, "Jobs", "OwnerUserId");
|
||||
|
||||
if (companyOwnershipExists || jobOwnershipExists)
|
||||
if (companyOwnershipExists || jobOwnershipExists || opportunityOwnershipExists)
|
||||
{
|
||||
if (companyOwnershipExists)
|
||||
{
|
||||
@@ -2123,6 +2124,11 @@ public static class StartupInitializationExtensions
|
||||
{
|
||||
adminDb.Database.ExecuteSqlRaw("UPDATE JobApplications SET OwnerUserId = {0} WHERE OwnerUserId IS NULL;", admin.Id);
|
||||
}
|
||||
|
||||
if (opportunityOwnershipExists)
|
||||
{
|
||||
adminDb.Database.ExecuteSqlRaw("UPDATE Jobs SET OwnerUserId = {0} WHERE OwnerUserId IS NULL;", admin.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -2173,6 +2179,14 @@ public static class StartupInitializationExtensions
|
||||
}
|
||||
}
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
|
||||
var backfilled = JobOpportunitySync.BackfillLegacyAsync(db).GetAwaiter().GetResult();
|
||||
if (backfilled > 0)
|
||||
app.Logger.LogInformation("Backfilled {Count} legacy job opportunities.", backfilled);
|
||||
}
|
||||
|
||||
var readiness = app.Services.GetRequiredService<IStartupReadiness>();
|
||||
readiness.MarkReady();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user