263 lines
10 KiB
C#
263 lines
10 KiB
C#
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace JobTrackerApi.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/admin/system")]
|
|
[Authorize(Roles = "Admin")]
|
|
public sealed class AdminSystemController : ControllerBase
|
|
{
|
|
private readonly IConfiguration _cfg;
|
|
private readonly AppPaths _paths;
|
|
private readonly JobTrackerContext _db;
|
|
private readonly ISummarizerService _summarizer;
|
|
private readonly IWebHostEnvironment _env;
|
|
private readonly IEmailSettingsResolver _emailSettings;
|
|
|
|
public AdminSystemController(IConfiguration cfg, AppPaths paths, JobTrackerContext db, ISummarizerService summarizer, IWebHostEnvironment env, IEmailSettingsResolver emailSettings)
|
|
{
|
|
_cfg = cfg;
|
|
_paths = paths;
|
|
_db = db;
|
|
_summarizer = summarizer;
|
|
_env = env;
|
|
_emailSettings = emailSettings;
|
|
}
|
|
|
|
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,
|
|
string Version,
|
|
string? CommitSha,
|
|
string? BuildStamp,
|
|
StorageStatusDto Storage,
|
|
EmailStatusDto Email,
|
|
DatabaseStatusDto Database,
|
|
RuntimeStatusDto Runtime,
|
|
AuthStatusDto Auth,
|
|
AiServiceMetrics Ai
|
|
);
|
|
|
|
private static string? NormalizeBuildMetadata(string? value)
|
|
{
|
|
var trimmed = (value ?? string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed)) return null;
|
|
|
|
// Ignore unresolved shell/compose placeholders that would otherwise leak
|
|
// directly into the admin UI, e.g. $(git rev-parse --short HEAD) or ${APP_COMMIT_SHA}.
|
|
if ((trimmed.StartsWith("$(") && trimmed.EndsWith(")")) || (trimmed.StartsWith("${") && trimmed.EndsWith("}")))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
[HttpPost("ai/probe")]
|
|
[HttpPost("summarizer/probe")]
|
|
public async Task<IActionResult> RunSummarizerProbe(CancellationToken cancellationToken)
|
|
{
|
|
await _summarizer.RunProbeAsync(cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpGet("email-settings")]
|
|
public async Task<ActionResult<EmailSettingsAdminDto>> GetEmailSettings(CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await _emailSettings.GetAdminDtoAsync(cancellationToken));
|
|
}
|
|
|
|
[HttpPut("email-settings")]
|
|
public async Task<ActionResult<EmailSettingsAdminDto>> UpdateEmailSettings([FromBody] UpdateEmailSettingsRequest request, CancellationToken cancellationToken)
|
|
{
|
|
if (request.Port <= 0) return BadRequest("SMTP port must be greater than 0.");
|
|
if (request.TimeoutMs <= 0) return BadRequest("SMTP timeout must be greater than 0.");
|
|
return Ok(await _emailSettings.UpdateAsync(request, cancellationToken));
|
|
}
|
|
|
|
[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);
|
|
|
|
var jobCount = 0;
|
|
var deletedCount = 0;
|
|
var companies = 0;
|
|
string? statusWarning = null;
|
|
try
|
|
{
|
|
jobCount = await _db.JobApplications.AsNoTracking().CountAsync(cancellationToken);
|
|
deletedCount = await _db.JobApplications.AsNoTracking().CountAsync(x => x.IsDeleted, cancellationToken);
|
|
companies = await _db.Companies.AsNoTracking().CountAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
statusWarning = $"Data query failed: {ex.GetType().Name}";
|
|
}
|
|
|
|
AiServiceMetrics ai;
|
|
try
|
|
{
|
|
ai = await _summarizer.GetMetricsAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ai = new AiServiceMetrics(
|
|
Healthy: false,
|
|
Model: null,
|
|
Device: null,
|
|
GpuAvailable: false,
|
|
GpuName: null,
|
|
OcrAvailable: false,
|
|
OcrLanguages: null,
|
|
OllamaConfigured: null,
|
|
OllamaReachable: null,
|
|
OllamaModel: null,
|
|
OllamaModelAvailable: null,
|
|
HealthLatencyMs: null,
|
|
ProbeLatencyMs: null,
|
|
LastProbeAt: null,
|
|
LastProbeSuccessAt: null,
|
|
LastProbeFailureAt: null,
|
|
ProbeFailures: 0,
|
|
Requests: 0,
|
|
CacheHits: 0,
|
|
CacheMisses: 0,
|
|
Failures: 0,
|
|
AverageLatencyMs: null,
|
|
OcrRequests: 0,
|
|
OcrFailures: 0,
|
|
AverageOcrLatencyMs: null,
|
|
LastOcrSuccessAt: null,
|
|
LastOcrFailureAt: null,
|
|
LastSuccessAt: null,
|
|
LastFailureAt: null,
|
|
LastError: ex.Message);
|
|
}
|
|
|
|
var version = NormalizeBuildMetadata(_cfg["App:Version"]);
|
|
if (string.IsNullOrWhiteSpace(version))
|
|
{
|
|
version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
|
|
}
|
|
|
|
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 && jobCount == 0 && companies == 0)
|
|
{
|
|
dbWarning = "Connected, but no data is present yet. Check whether this is the intended production database.";
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(statusWarning))
|
|
{
|
|
dbWarning = string.IsNullOrWhiteSpace(dbWarning)
|
|
? statusWarning
|
|
: $"{dbWarning} {statusWarning}";
|
|
}
|
|
|
|
var gmailConfigured = !string.IsNullOrWhiteSpace((_cfg["Google:GmailClientSecret"] ?? string.Empty).Trim())
|
|
&& !string.IsNullOrWhiteSpace((_cfg["Google:GmailRedirectUri"] ?? string.Empty).Trim());
|
|
var emailSettings = await _emailSettings.GetSnapshotAsync(cancellationToken);
|
|
|
|
return Ok(new SystemStatusDto(
|
|
Environment: _env.EnvironmentName,
|
|
ContentRoot: _env.ContentRootPath,
|
|
Version: version,
|
|
CommitSha: string.IsNullOrWhiteSpace(commitSha) ? null : commitSha,
|
|
BuildStamp: string.IsNullOrWhiteSpace(buildStamp) ? null : buildStamp,
|
|
Storage: new StorageStatusDto(
|
|
DataRoot: _paths.DataRoot,
|
|
DbPath: dbPath,
|
|
DbExists: dbFile.Exists,
|
|
DbSizeBytes: dbFile.Exists ? dbFile.Length : null,
|
|
CompanyCount: companies,
|
|
JobCount: jobCount,
|
|
DeletedCount: deletedCount
|
|
),
|
|
Email: new EmailStatusDto(
|
|
Enabled: emailSettings.Enabled,
|
|
Host: emailSettings.Host,
|
|
Port: emailSettings.Port,
|
|
EnableSsl: emailSettings.EnableSsl,
|
|
From: emailSettings.From,
|
|
FromName: emailSettings.FromName
|
|
),
|
|
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
|
|
),
|
|
Ai: ai
|
|
));
|
|
}
|
|
}
|