Add attachment metadata and overview strategy snapshot

This commit is contained in:
cesnimda
2026-03-23 22:46:44 +01:00
parent 93f5c9beb7
commit 603f5e8b74
8 changed files with 190 additions and 18 deletions
@@ -27,7 +27,7 @@ namespace JobTrackerApi.Controllers
_db = db;
}
public sealed record AttachmentDto(int Id, string FileName, DateTime UploadDate, string FileType, long FileSize);
public sealed record AttachmentDto(int Id, string FileName, DateTime UploadDate, string FileType, long FileSize, string? Purpose, bool UseForAi);
// Child entities are accessed by raw integer ids in a few endpoints below.
// Always resolve them through the parent JobApplication query so the global job-level
@@ -46,6 +46,17 @@ namespace JobTrackerApi.Controllers
return $"{DateTime.UtcNow:yyyyMMddHHmmssfff}-{suffix}{ext}";
}
private static string GuessPurpose(string fileName)
{
var n = (fileName ?? string.Empty).ToLowerInvariant();
if (n.Contains("cover")) return "cover-letter";
if (n.Contains("resume") || n.Contains("résumé") || n.Contains(" cv") || n.EndsWith("cv.pdf")) return "resume";
if (n.Contains("portfolio")) return "portfolio";
if (n.Contains("case") || n.Contains("sample")) return "case-study";
if (n.Contains("cert")) return "certificate";
return "other";
}
[HttpGet("{jobId:int}")]
public async Task<ActionResult<List<AttachmentDto>>> ListForJob([FromRoute] int jobId, CancellationToken cancellationToken)
{
@@ -56,7 +67,7 @@ namespace JobTrackerApi.Controllers
.AsNoTracking()
.Where(a => a.JobApplicationId == jobId)
.OrderByDescending(a => a.UploadDate)
.Select(a => new AttachmentDto(a.Id, a.FileName, a.UploadDate, a.FileType, a.FileSize))
.Select(a => new AttachmentDto(a.Id, a.FileName, a.UploadDate, a.FileType, a.FileSize, a.Purpose, a.UseForAi))
.ToListAsync(cancellationToken);
return Ok(items);
@@ -76,17 +87,32 @@ namespace JobTrackerApi.Controllers
return PhysicalFile(att.FilePath, contentType, fileName);
}
public sealed record RenameAttachmentRequest(string FileName);
public sealed record UpdateAttachmentRequest(string? FileName, string? Purpose, bool? UseForAi);
[HttpPatch("{id:int}")]
public async Task<IActionResult> Rename([FromRoute] int id, [FromBody] RenameAttachmentRequest request, CancellationToken cancellationToken)
public async Task<IActionResult> Rename([FromRoute] int id, [FromBody] UpdateAttachmentRequest request, CancellationToken cancellationToken)
{
var att = await FindOwnedAttachmentAsync(id, cancellationToken);
if (att is null) return NotFound();
var name = Path.GetFileName((request.FileName ?? "").Trim());
if (name.Length == 0) return BadRequest("FileName is required.");
if (request.UseForAi is not null)
{
att.UseForAi = request.UseForAi.Value;
}
if (!string.IsNullOrWhiteSpace(request.Purpose))
{
att.Purpose = request.Purpose.Trim().ToLowerInvariant();
}
var rawName = (request.FileName ?? string.Empty).Trim();
if (rawName.Length == 0)
{
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}
var name = Path.GetFileName(rawName);
var ext = Path.GetExtension(name);
if (!AllowedExtensions.Contains(ext))
return BadRequest("That file type is not allowed.");
@@ -166,7 +192,9 @@ namespace JobTrackerApi.Controllers
FilePath = path,
UploadDate = DateTime.Now,
FileType = string.IsNullOrWhiteSpace(file.ContentType) ? "application/octet-stream" : file.ContentType,
FileSize = file.Length
FileSize = file.Length,
Purpose = GuessPurpose(displayName),
UseForAi = true,
});
}
@@ -99,6 +99,10 @@ namespace JobTrackerApi.Controllers
{
query = query.Where(a => allowedIds.Contains(a.Id));
}
else
{
query = query.Where(a => a.UseForAi);
}
var attachments = await query
.OrderByDescending(a => a.UploadDate)
+4
View File
@@ -676,6 +676,8 @@ CREATE TABLE IF NOT EXISTS "GmailConnections" (
EnsureColumn(conn, "Correspondences", "Subject", "ALTER TABLE Correspondences ADD COLUMN Subject TEXT NULL;");
EnsureColumn(conn, "Correspondences", "Channel", "ALTER TABLE Correspondences ADD COLUMN Channel TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalMessageId", "ALTER TABLE Correspondences ADD COLUMN ExternalMessageId TEXT NULL;");
EnsureColumn(conn, "Attachments", "Purpose", "ALTER TABLE Attachments ADD COLUMN Purpose TEXT NULL;");
EnsureColumn(conn, "Attachments", "UseForAi", "ALTER TABLE Attachments ADD COLUMN UseForAi INTEGER NOT NULL DEFAULT 1;");
// Ensure data folder exists before creating/opening SQLite files.
Directory.CreateDirectory(paths.DataRoot);
@@ -730,6 +732,8 @@ CREATE TABLE IF NOT EXISTS "GmailConnections" (
EnsureMySqlColumn(conn, "Companies", "OwnerUserId", "ALTER TABLE `Companies` ADD COLUMN `OwnerUserId` varchar(255) NULL;");
EnsureMySqlColumn(conn, "JobApplications", "OwnerUserId", "ALTER TABLE `JobApplications` ADD COLUMN `OwnerUserId` varchar(255) NULL;");
EnsureMySqlColumn(conn, "JobApplications", "LastReminderEmailSentAt", "ALTER TABLE `JobApplications` ADD COLUMN `LastReminderEmailSentAt` datetime NULL;");
EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;");
EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;");
if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId"))
{