diff --git a/src/ClaudeDo.Data/Wire.cs b/src/ClaudeDo.Data/Wire.cs index 9d649a9e..d62150db 100644 --- a/src/ClaudeDo.Data/Wire.cs +++ b/src/ClaudeDo.Data/Wire.cs @@ -222,7 +222,7 @@ public sealed record LaunchSpec( // action -- confirms with a count, then cancels each one before closing the pane. public record HandlerOpenTaskDto(string Id, string Title); -public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet); +public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet, string? FallbackListId = null); public record TicketConnectionDto(bool Ok, string? UserName, IReadOnlyList Scopes, string? Error); public record TicketProjectDto(int Id, string Title, string DepartmentName); public record TicketImportResultDto(int Examined, int Created); diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 05b7684b..baf8568c 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -1028,11 +1028,15 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub return Task.CompletedTask; } - public async Task RunDailyPrepNow() + public Task RunDailyPrepNow() => RunPrimeNowAsync(ClaudeDo.Data.Models.PrimeActionKind.FillMyDay); + + /// The "Tickets + plan day" button: ticket import, then FillMyDay, through the same Prime gate. + public Task RunTicketImportNow() => RunPrimeNowAsync(ClaudeDo.Data.Models.PrimeActionKind.ImportTickets); + + // A button means its own kind no matter how the schedules are configured — hence a synthetic schedule. + private async Task RunPrimeNowAsync(ClaudeDo.Data.Models.PrimeActionKind kind) { - var schedule = new PrimeScheduleDto( - Guid.Empty, 0, TimeSpan.Zero, true, null, null, - ClaudeDo.Data.Models.PrimeActionKind.FillMyDay); + var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null, kind); var firedAt = DateTimeOffset.Now; var outcome = await _primeRunner.FireAsync(schedule, Context.ConnectionAborted); await _broadcaster.PrimeFired(Guid.Empty, outcome.Success, outcome.Message, firedAt); @@ -1363,8 +1367,19 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub // Liefert die EFFEKTIVE Base-URL (inkl. ergänztem Schema), nicht den rohen Feldwert: sonst // sieht der Nutzer im Tab weiter "host.local", während der Client "http://host.local" ruft. - public TicketSettingsDto GetTicketSettings() - => new(_ticketConfig.BaseUrl, _ticketConfig.Token is not null); + public async Task GetTicketSettings() + { + using var ctx = _dbFactory.CreateDbContext(); + var settings = await new AppSettingsRepository(ctx).GetAsync(Context.ConnectionAborted); + return new TicketSettingsDto(_ticketConfig.BaseUrl, _ticketConfig.Token is not null, settings.TicketFallbackListId); + } + + /// Sets the list that import_ticket uses when Claude passes no list; null clears it. + public async Task SetTicketFallbackList(string? listId) + { + using var ctx = _dbFactory.CreateDbContext(); + await new AppSettingsRepository(ctx).SetTicketFallbackListIdAsync(listId, Context.ConnectionAborted); + } public void SetTicketApiBaseUrl(string? baseUrl) { diff --git a/tests/ClaudeDo.Worker.Tests/Hub/TicketFallbackHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/TicketFallbackHubTests.cs new file mode 100644 index 00000000..757d9e2c --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Hub/TicketFallbackHubTests.cs @@ -0,0 +1,50 @@ +using ClaudeDo.Worker; +using ClaudeDo.Worker.Config; +using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Tests.Infrastructure; +using ClaudeDo.Worker.Tickets; +using Xunit; + +namespace ClaudeDo.Worker.Tests.Hub; + +public sealed class TicketFallbackHubTests : IDisposable +{ + private readonly DbFixture _db = new(); + private readonly string _patDir = Path.Combine(Path.GetTempPath(), "cdo-hubpat-" + Guid.NewGuid().ToString("N")); + + public void Dispose() + { + _db.Dispose(); + try { Directory.Delete(_patDir, recursive: true); } catch { /* best effort */ } + } + + private WorkerHub CreateHub() + { + var ticketConfig = new TicketSystemConfig( + new WorkerConfig(), new TicketPatStore(new DpapiTokenStore(Path.Combine(_patDir, "ticket.pat")))); + var hub = new WorkerHub( + null!, null!, null!, null!, new HubBroadcaster(new CapturingHubContext()), _db.CreateFactory(), + null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, + null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new DpapiTokenStore("unused.token")), + new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!, + ticketConfig: ticketConfig); + hub.Clients = new FakeHubCallerClients(new RecordingClientProxy()); + hub.Context = new FakeHubCallerContext(); + return hub; + } + + [Fact] + public async Task Fallback_list_set_through_the_hub_comes_back_in_the_ticket_settings() + { + var hub = CreateHub(); + + await hub.SetTicketFallbackList("l-fallback"); + var dto = await hub.GetTicketSettings(); + + Assert.Equal("l-fallback", dto.FallbackListId); + Assert.False(dto.TokenSet); + + await hub.SetTicketFallbackList(null); + Assert.Null((await hub.GetTicketSettings()).FallbackListId); + } +}