From 1770b67713b96c7cba67df2fed2a2ff6a230e01c Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 10 Sep 2026 15:39:58 +0200 Subject: [PATCH] fix(tickets): Import broadcastet TaskUpdated fuer neue Tasks ImportTickets sendete nur ListUpdated. Die UI liest das als Aenderung der Listen-Metadaten und patcht nur bestehende Zeilen, also tauchten die importierten Tasks erst nach Schliessen und Neuoeffnen der Liste auf. TicketImportResult liefert jetzt die angelegten Task-Ids, der Hub broadcastet pro Id ein TaskUpdated - derselbe Pfad, den jeder andere out-of-band Creator nutzt. --- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 5 +++++ src/ClaudeDo.Worker/Tickets/TicketImportService.cs | 13 +++++++------ .../Tickets/TicketImportServiceTests.cs | 8 ++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 5f8b24f0..a5028c38 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -1394,6 +1394,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub try { var result = await _ticketImport.ImportAsync(listId, Context.ConnectionAborted); + // ListUpdated allein reicht nicht: die UI liest das als "Listen-Metadaten geaendert" + // und patcht nur bestehende Zeilen. Neue Tasks tauchen nur ueber TaskUpdated auf, wie + // bei jedem anderen out-of-band Creator (add_task und Co.). + foreach (var taskId in result.CreatedTaskIds) + await _broadcaster.TaskUpdated(taskId); await _broadcaster.ListUpdated(listId); return new TicketImportResultDto(result.Examined, result.Created); } diff --git a/src/ClaudeDo.Worker/Tickets/TicketImportService.cs b/src/ClaudeDo.Worker/Tickets/TicketImportService.cs index 985ffc73..d7e9c818 100644 --- a/src/ClaudeDo.Worker/Tickets/TicketImportService.cs +++ b/src/ClaudeDo.Worker/Tickets/TicketImportService.cs @@ -6,7 +6,7 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tickets; -public sealed record TicketImportResult(int Examined, int Created); +public sealed record TicketImportResult(int Examined, int Created, IReadOnlyList CreatedTaskIds); /// /// Holt das Board des verknüpften Ticket-Projekts und legt für jedes offene, dem Token-Inhaber @@ -61,16 +61,17 @@ public sealed class TicketImportService var knownSet = known.ToHashSet(StringComparer.OrdinalIgnoreCase); var repo = new TaskRepository(ctx); - var created = 0; + var created = new List(); foreach (var ticket in mine) { var reference = $"bandel:{ticket.Id}"; if (!knownSet.Add(reference)) continue; + var taskId = Guid.NewGuid().ToString(); await repo.AddAsync(new TaskEntity { - Id = Guid.NewGuid().ToString(), + Id = taskId, ListId = listId, Title = ticket.Title, Description = BuildDescription(ticket), @@ -79,14 +80,14 @@ public sealed class TicketImportService TicketRef = reference, CreatedBy = "ticketsystem", }, ct); - created++; + created.Add(taskId); } _logger.LogInformation( "Ticket-Import für Liste {list_id}: {examined} geprüft, {created} neue Tasks", - listId, board.Count, created); + listId, board.Count, created.Count); - return new TicketImportResult(board.Count, created); + return new TicketImportResult(board.Count, created.Count, created); } /// diff --git a/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportServiceTests.cs index 08b390b2..6a61547a 100644 --- a/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportServiceTests.cs @@ -43,6 +43,14 @@ public sealed class TicketImportServiceTests : IDisposable .ToListAsync(); Assert.Equal(["bandel:1", "bandel:3"], refs.Order()); + // Ohne die Ids kann der Hub kein TaskUpdated broadcasten und die offene Liste + // zeigt die neuen Tasks erst nach Schließen/Öffnen. + var createdIds = await ctx.Tasks + .Where(t => t.CreatedBy == "ticketsystem") + .Select(t => t.Id) + .ToListAsync(); + Assert.Equal(createdIds.Order(), result.CreatedTaskIds.Order()); + var created = await ctx.Tasks.Where(t => t.TicketRef == "bandel:1").SingleAsync(); Assert.Equal("Fix X", created.Title); Assert.Equal(TaskStatus.Idle, created.Status);