feat(worker): Ticket-Import pro Liste

This commit is contained in:
mika kuns
2026-08-27 13:05:38 +02:00
parent 5aafeb6c3d
commit 2e236736a7
3 changed files with 288 additions and 0 deletions
+1
View File
@@ -221,6 +221,7 @@ builder.Services.AddSingleton<TicketSystemConfig>();
builder.Services.AddHttpClient("tickets");
builder.Services.AddSingleton<TicketClientFactory>();
builder.Services.AddSingleton<TicketStatusSync>();
builder.Services.AddSingleton<TicketImportService>();
if (cfg.OnlineInbox.Enabled)
{
@@ -0,0 +1,95 @@
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tickets;
public sealed record TicketImportResult(int Examined, int Created);
/// <summary>
/// Holt das Board des verknüpften Ticket-Projekts und legt für jedes offene, dem Token-Inhaber
/// zugewiesene Ticket einen Idle-Task an. Bereits importierte Tickets werden übersprungen und
/// NICHT aktualisiert — ein erneuter Import würde sonst Titel/Beschreibung überschreiben und
/// damit die am Task gemachten Notizen zerstören.
/// </summary>
public sealed class TicketImportService
{
private const int StatusOffen = 1;
private readonly TicketClientFactory _clients;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly ILogger<TicketImportService> _logger;
public TicketImportService(
TicketClientFactory clients,
IDbContextFactory<ClaudeDoDbContext> dbFactory,
ILogger<TicketImportService> logger)
{
_clients = clients;
_dbFactory = dbFactory;
_logger = logger;
}
public async Task<TicketImportResult> ImportAsync(string listId, CancellationToken ct)
{
var client = _clients.Create()
?? throw new TicketApiException("Ticketsystem ist nicht eingerichtet (Base-URL oder Token fehlt).");
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
var config = await ctx.ListConfigs.AsNoTracking()
.FirstOrDefaultAsync(c => c.ListId == listId, ct);
var projectId = config?.TicketProjectId
?? throw new TicketApiException("Diese Liste ist mit keinem Ticket-Projekt verknüpft.");
var identity = await client.GetIdentityAsync(ct);
var board = await client.GetProjectBoardAsync(projectId, ct);
var mine = board
.Where(t => t.StatusId == StatusOffen)
.Where(t => string.Equals(t.AssigneeName, identity.UserName, StringComparison.OrdinalIgnoreCase))
.ToList();
var known = await ctx.Tasks.AsNoTracking()
.Where(t => t.ListId == listId && t.TicketRef != null)
.Select(t => t.TicketRef!)
.ToListAsync(ct);
var knownSet = known.ToHashSet(StringComparer.OrdinalIgnoreCase);
var repo = new TaskRepository(ctx);
var created = 0;
foreach (var ticket in mine)
{
var reference = $"bandel:{ticket.Id}";
if (!knownSet.Add(reference)) continue;
await repo.AddAsync(new TaskEntity
{
Id = Guid.NewGuid().ToString(),
ListId = listId,
Title = ticket.Title,
Description = BuildDescription(ticket),
Status = TaskStatus.Idle,
CreatedAt = DateTime.UtcNow,
TicketRef = reference,
CreatedBy = "ticketsystem",
}, ct);
created++;
}
_logger.LogInformation(
"Ticket-Import für Liste {list_id}: {examined} geprüft, {created} neue Tasks",
listId, board.Count, created);
return new TicketImportResult(board.Count, created);
}
private static string BuildDescription(TicketBoardItem ticket)
{
var body = string.IsNullOrWhiteSpace(ticket.Description) ? "" : ticket.Description.Trim() + "\n\n";
return body + $"Ticket #{ticket.Id}";
}
}