feat(hub): Azure DevOps PAT settings and connection test

This commit is contained in:
mika kuns
2026-10-01 12:06:56 +02:00
parent bc959f25b3
commit fed0b6ac51
5 changed files with 70 additions and 1 deletions
+3
View File
@@ -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<string> 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);
@@ -252,6 +252,10 @@ public interface IWorkerClient : INotifyPropertyChanged
Task SetTicketTokenAsync(string token);
Task ClearTicketTokenAsync();
Task<TicketConnectionDto?> TestTicketConnectionAsync();
Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync();
Task SetAzureDevOpsTokenAsync(string token);
Task ClearAzureDevOpsTokenAsync();
Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync();
Task<List<TicketProjectDto>> GetTicketProjectsAsync();
/// <summary>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
+12
View File
@@ -776,6 +776,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public Task<TicketConnectionDto?> TestTicketConnectionAsync()
=> TryInvokeAsync<TicketConnectionDto>("TestTicketConnection");
public Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync()
=> TryInvokeAsync<AzureDevOpsSettingsDto>("GetAzureDevOpsSettings");
public Task SetAzureDevOpsTokenAsync(string token)
=> InvokeTimedAsync("SetAzureDevOpsToken", () => _hub.InvokeAsync("SetAzureDevOpsToken", token));
public Task ClearAzureDevOpsTokenAsync()
=> InvokeTimedAsync("ClearAzureDevOpsToken", () => _hub.InvokeAsync("ClearAzureDevOpsToken"));
public Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync()
=> TryInvokeAsync<AzureDevOpsConnectionDto>("TestAzureDevOpsConnection");
public async Task<List<TicketProjectDto>> GetTicketProjectsAsync()
=> await TryInvokeAsync<List<TicketProjectDto>>("GetTicketProjects") ?? [];
+47 -1
View File
@@ -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();
/// <summary>Reads the first company repo's ADO repository (default branch). Errors are answers, not throws — same reason as TestTicketConnection.</summary>
public async Task<AzureDevOpsConnectionDto> 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<string> 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<List<TicketProjectDto>> GetTicketProjects()
{
var client = _ticketClients.Create();
@@ -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<TicketConnectionDto?> TestTicketConnectionAsync() => Task.FromResult<TicketConnectionDto?>(null);
public virtual Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync() => Task.FromResult<AzureDevOpsSettingsDto?>(null);
public virtual Task SetAzureDevOpsTokenAsync(string token) => Task.CompletedTask;
public virtual Task ClearAzureDevOpsTokenAsync() => Task.CompletedTask;
public virtual Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync() => Task.FromResult<AzureDevOpsConnectionDto?>(null);
public virtual Task<List<TicketProjectDto>> GetTicketProjectsAsync() => Task.FromResult(new List<TicketProjectDto>());
public virtual Task<TicketImportResultDto> ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));