diff --git a/src/ClaudeDo.Data/Wire.cs b/src/ClaudeDo.Data/Wire.cs index 714ffad4..17fb7f1b 100644 --- a/src/ClaudeDo.Data/Wire.cs +++ b/src/ClaudeDo.Data/Wire.cs @@ -225,5 +225,8 @@ public record HandlerOpenTaskDto(string Id, string Title); public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet, string? FallbackListId = null); public record TicketConnectionDto(bool Ok, string? UserName, IReadOnlyList Scopes, string? Error); +public record AzureDevOpsSettingsDto(bool TokenSet); +// Repo = the company repo the test ran against (first list whose origin matches), DefaultBranch = its ADO default. +public record AzureDevOpsConnectionDto(bool Ok, string? Repo, string? DefaultBranch, string? Error); public record TicketProjectDto(int Id, string Title, string DepartmentName); public record TicketImportResultDto(int Examined, int Created); diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index a7f046ff..f830e511 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -252,6 +252,10 @@ public interface IWorkerClient : INotifyPropertyChanged Task SetTicketTokenAsync(string token); Task ClearTicketTokenAsync(); Task TestTicketConnectionAsync(); + Task GetAzureDevOpsSettingsAsync(); + Task SetAzureDevOpsTokenAsync(string token); + Task ClearAzureDevOpsTokenAsync(); + Task TestAzureDevOpsConnectionAsync(); Task> GetTicketProjectsAsync(); /// Imports open tickets assigned to the token owner into the given list. Throws /// HubException (with a user-readable message) on failure so the caller can surface it via diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 819f6b85..698a0aa8 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -776,6 +776,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public Task TestTicketConnectionAsync() => TryInvokeAsync("TestTicketConnection"); + public Task GetAzureDevOpsSettingsAsync() + => TryInvokeAsync("GetAzureDevOpsSettings"); + + public Task SetAzureDevOpsTokenAsync(string token) + => InvokeTimedAsync("SetAzureDevOpsToken", () => _hub.InvokeAsync("SetAzureDevOpsToken", token)); + + public Task ClearAzureDevOpsTokenAsync() + => InvokeTimedAsync("ClearAzureDevOpsToken", () => _hub.InvokeAsync("ClearAzureDevOpsToken")); + + public Task TestAzureDevOpsConnectionAsync() + => TryInvokeAsync("TestAzureDevOpsConnection"); + public async Task> GetTicketProjectsAsync() => await TryInvokeAsync>("GetTicketProjects") ?? []; diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 69a52302..dbea9f36 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -18,6 +18,7 @@ using ClaudeDo.Worker.Report; using ClaudeDo.Worker.Report.Interfaces; using ClaudeDo.Worker.Skills; using ClaudeDo.Worker.State; +using ClaudeDo.Worker.AzureDevOps; using ClaudeDo.Worker.Tickets; using ClaudeDo.Worker.Usage; using ClaudeDo.Worker.Usage.Interfaces; @@ -75,6 +76,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub private readonly HandlerRestartCoordinator? _handlerRestart; private readonly HandlerRoundState? _roundState; private readonly ConnectedClientTracker? _clientTracker; + private readonly AzureDevOpsPatStore? _azdoPat; + private readonly AzureDevOpsClient? _azdo; public WorkerHub( QueueService queue, @@ -116,7 +119,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub TicketImportService ticketImport = null!, HandlerRestartCoordinator? handlerRestart = null, HandlerRoundState? roundState = null, - ConnectedClientTracker? clientTracker = null) + ConnectedClientTracker? clientTracker = null, + AzureDevOpsPatStore? azdoPat = null, + AzureDevOpsClient? azdo = null) { _queue = queue; _waker = waker; @@ -158,6 +163,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub _handlerRestart = handlerRestart; _roundState = roundState; _clientTracker = clientTracker; + _azdoPat = azdoPat; + _azdo = azdo; } // Tracks connected UI clients so an out-of-band caller (external MCP's start_list_handler) @@ -1421,6 +1428,45 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub } } + public AzureDevOpsSettingsDto GetAzureDevOpsSettings() => new(_azdoPat?.Store.Exists() == true); + + public void SetAzureDevOpsToken(string token) + { + if (_azdoPat is null) return; + if (string.IsNullOrWhiteSpace(token)) _azdoPat.Store.Clear(); + else _azdoPat.Store.Save(token.Trim()); + } + + public void ClearAzureDevOpsToken() => _azdoPat?.Store.Clear(); + + /// Reads the first company repo's ADO repository (default branch). Errors are answers, not throws — same reason as TestTicketConnection. + public async Task TestAzureDevOpsConnection() + { + if (_azdo is null) return new AzureDevOpsConnectionDto(false, null, null, "Azure DevOps ist im Worker nicht registriert."); + try + { + var git = _git ?? new Data.Git.GitService(); + List dirs; + await using (var ctx = await _dbFactory.CreateDbContextAsync(Context.ConnectionAborted)) + dirs = await ctx.Lists.AsNoTracking().Where(l => l.WorkingDir != null).Select(l => l.WorkingDir!).ToListAsync(Context.ConnectionAborted); + + foreach (var dir in dirs) + { + if (!Directory.Exists(dir)) continue; + var remote = await git.GetRemoteUrlAsync(dir, "origin", Context.ConnectionAborted); + if (!Git.CompanyRepo.Matches(remote, _cfg.CompanyRemotePattern) || AzureDevOpsClient.ParseRemote(remote) is not { } repo) + continue; + var branch = await _azdo.GetDefaultBranchAsync(repo, Context.ConnectionAborted); + return new AzureDevOpsConnectionDto(true, repo.Repo, branch, null); + } + return new AzureDevOpsConnectionDto(false, null, null, "Keine Liste zeigt auf ein Firmen-Repo (origin mit " + _cfg.CompanyRemotePattern + ")."); + } + catch (Exception ex) + { + return new AzureDevOpsConnectionDto(false, null, null, ex.Message); + } + } + public async Task> GetTicketProjects() { var client = _ticketClients.Create(); diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs index a8826097..f4bb5306 100644 --- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs +++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs @@ -227,6 +227,10 @@ public abstract class StubWorkerClient : IWorkerClient public virtual Task SetTicketTokenAsync(string token) => Task.CompletedTask; public virtual Task ClearTicketTokenAsync() => Task.CompletedTask; public virtual Task TestTicketConnectionAsync() => Task.FromResult(null); + public virtual Task GetAzureDevOpsSettingsAsync() => Task.FromResult(null); + public virtual Task SetAzureDevOpsTokenAsync(string token) => Task.CompletedTask; + public virtual Task ClearAzureDevOpsTokenAsync() => Task.CompletedTask; + public virtual Task TestAzureDevOpsConnectionAsync() => Task.FromResult(null); public virtual Task> GetTicketProjectsAsync() => Task.FromResult(new List()); public virtual Task ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));