feat: improve admin observability and translation-first summaries
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -29,6 +30,9 @@ public sealed class AdminSystemController : ControllerBase
|
||||
|
||||
public sealed record StorageStatusDto(string DataRoot, string DbPath, bool DbExists, long? DbSizeBytes, int CompanyCount, int JobCount, int DeletedCount);
|
||||
public sealed record EmailStatusDto(bool Enabled, string? Host, int Port, bool EnableSsl, string? From, string? FromName);
|
||||
public sealed record DatabaseStatusDto(string Provider, bool LooksConfigured, bool CanConnect, string? Target, bool UsesFileStorage, string? Warning);
|
||||
public sealed record RuntimeStatusDto(string Framework, string OSDescription, string ProcessArchitecture, string? MachineName);
|
||||
public sealed record AuthStatusDto(bool Required, bool HasJwtKey, bool GoogleConfigured, bool GmailConfigured);
|
||||
public sealed record SystemStatusDto(
|
||||
string Environment,
|
||||
string ContentRoot,
|
||||
@@ -37,6 +41,9 @@ public sealed class AdminSystemController : ControllerBase
|
||||
string? BuildStamp,
|
||||
StorageStatusDto Storage,
|
||||
EmailStatusDto Email,
|
||||
DatabaseStatusDto Database,
|
||||
RuntimeStatusDto Runtime,
|
||||
AuthStatusDto Auth,
|
||||
SummarizerMetrics Summarizer
|
||||
);
|
||||
|
||||
@@ -65,6 +72,8 @@ public sealed class AdminSystemController : ControllerBase
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<SystemStatusDto>> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
var provider = (_cfg["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
||||
var usesFileStorage = provider is not "mysql" and not "mariadb";
|
||||
var dbPath = _paths.GetDbPath();
|
||||
var dbFile = new FileInfo(dbPath);
|
||||
|
||||
@@ -80,6 +89,53 @@ public sealed class AdminSystemController : ControllerBase
|
||||
|
||||
var commitSha = NormalizeBuildMetadata(_cfg["App:CommitSha"]);
|
||||
var buildStamp = NormalizeBuildMetadata(_cfg["App:BuildStamp"]);
|
||||
var connectionString = _cfg.GetConnectionString("JobTracker") ?? string.Empty;
|
||||
var looksConfigured = !string.IsNullOrWhiteSpace(connectionString) || usesFileStorage;
|
||||
|
||||
string? dbTarget;
|
||||
if (usesFileStorage)
|
||||
{
|
||||
dbTarget = dbPath;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
dbTarget = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
dbTarget = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(part => part.Trim())
|
||||
.FirstOrDefault(part => part.StartsWith("server=", StringComparison.OrdinalIgnoreCase)
|
||||
|| part.StartsWith("host=", StringComparison.OrdinalIgnoreCase)
|
||||
|| part.StartsWith("database=", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
bool canConnect;
|
||||
try
|
||||
{
|
||||
canConnect = await _db.Database.CanConnectAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
canConnect = false;
|
||||
}
|
||||
|
||||
string? dbWarning = null;
|
||||
if (!looksConfigured)
|
||||
{
|
||||
dbWarning = "Connection string is missing.";
|
||||
}
|
||||
else if (!canConnect)
|
||||
{
|
||||
dbWarning = "Database connection failed.";
|
||||
}
|
||||
else if (!usesFileStorage && jobs.Count == 0 && companies == 0)
|
||||
{
|
||||
dbWarning = "Connected, but no data is present yet. Check whether this is the intended production database.";
|
||||
}
|
||||
|
||||
var gmailConfigured = !string.IsNullOrWhiteSpace((_cfg["Google:GmailClientSecret"] ?? string.Empty).Trim())
|
||||
&& !string.IsNullOrWhiteSpace((_cfg["Google:GmailRedirectUri"] ?? string.Empty).Trim());
|
||||
|
||||
return Ok(new SystemStatusDto(
|
||||
Environment: _env.EnvironmentName,
|
||||
@@ -104,6 +160,26 @@ public sealed class AdminSystemController : ControllerBase
|
||||
From: (_cfg["Email:From"] ?? string.Empty).Trim(),
|
||||
FromName: (_cfg["Email:FromName"] ?? string.Empty).Trim()
|
||||
),
|
||||
Database: new DatabaseStatusDto(
|
||||
Provider: provider,
|
||||
LooksConfigured: looksConfigured,
|
||||
CanConnect: canConnect,
|
||||
Target: dbTarget,
|
||||
UsesFileStorage: usesFileStorage,
|
||||
Warning: dbWarning
|
||||
),
|
||||
Runtime: new RuntimeStatusDto(
|
||||
Framework: RuntimeInformation.FrameworkDescription,
|
||||
OSDescription: RuntimeInformation.OSDescription,
|
||||
ProcessArchitecture: RuntimeInformation.ProcessArchitecture.ToString(),
|
||||
MachineName: Environment.MachineName
|
||||
),
|
||||
Auth: new AuthStatusDto(
|
||||
Required: _cfg.GetValue("Auth:Require", false),
|
||||
HasJwtKey: !string.IsNullOrWhiteSpace((_cfg["Auth:JwtKey"] ?? string.Empty).Trim()),
|
||||
GoogleConfigured: !string.IsNullOrWhiteSpace((_cfg["Auth:GoogleClientId"] ?? string.Empty).Trim()),
|
||||
GmailConfigured: gmailConfigured
|
||||
),
|
||||
Summarizer: summarizer
|
||||
));
|
||||
}
|
||||
|
||||
@@ -71,6 +71,20 @@ namespace JobTrackerApi.Controllers
|
||||
return new string(value.Trim().ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray());
|
||||
}
|
||||
|
||||
private static string BuildSummarySource(JobApplication job)
|
||||
{
|
||||
// Prefer translated text for summaries and skill extraction so non-English
|
||||
// postings become easier to understand while keeping the original text intact.
|
||||
var parts = new[]
|
||||
{
|
||||
job.TranslatedDescription,
|
||||
job.Description,
|
||||
job.Notes
|
||||
};
|
||||
|
||||
return string.Join("\n\n", parts.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()));
|
||||
}
|
||||
|
||||
private static string? NormalizeTags(string? raw)
|
||||
{
|
||||
var normalized = SplitTags(raw)
|
||||
@@ -363,8 +377,9 @@ namespace JobTrackerApi.Controllers
|
||||
.MaxAsync(c => (DateTime?)c.Date, cancellationToken);
|
||||
|
||||
var d = RulesEngine.Evaluate(settings, job, now, lm);
|
||||
// Return persisted short summary and compute a fuller summary on demand for the details view.
|
||||
var full = await _summarizer.SummarizeAsync(job.Description ?? job.Notes ?? "", 250, 40);
|
||||
// Prefer translated content for the detailed summary so Norwegian postings
|
||||
// surface readable English analysis while the original text remains available.
|
||||
var full = await _summarizer.SummarizeAsync(BuildSummarySource(job), 250, 40);
|
||||
|
||||
return Ok(new JobApplicationDto(
|
||||
Id: job.Id,
|
||||
@@ -596,7 +611,7 @@ namespace JobTrackerApi.Controllers
|
||||
// Generate and persist a short summary at creation time to avoid repeated model calls.
|
||||
try
|
||||
{
|
||||
var shortSum = await _summarizer.SummarizeAsync(job.Description ?? job.Notes ?? "", 160, 60);
|
||||
var shortSum = await _summarizer.SummarizeAsync(BuildSummarySource(job), 160, 60);
|
||||
job.ShortSummary = shortSum;
|
||||
}
|
||||
catch
|
||||
@@ -752,11 +767,10 @@ namespace JobTrackerApi.Controllers
|
||||
|
||||
if (job is null) return NotFound();
|
||||
|
||||
var sourceText = string.Join("\n\n", new[] { job.Description, job.TranslatedDescription, job.Notes }
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
var sourceText = BuildSummarySource(job);
|
||||
if (string.IsNullOrWhiteSpace(sourceText))
|
||||
{
|
||||
return BadRequest("This job does not have enough description or notes to generate a summary and skills.");
|
||||
return BadRequest("This job does not have enough translated text, description, or notes to generate a summary and skills.");
|
||||
}
|
||||
|
||||
var tags = SkillTagger.Detect(sourceText)
|
||||
@@ -1710,7 +1724,7 @@ Candidate master CV:
|
||||
))
|
||||
.ToList();
|
||||
|
||||
return Ok(new DuplicateCheckResult(matches.Count > 0, matches));
|
||||
return Ok(new DuplicateCheckResult(matches.Any(), matches));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/followup-draft")]
|
||||
|
||||
Reference in New Issue
Block a user