feat(hub): Azure DevOps PAT settings and connection test
This commit is contained in:
@@ -225,5 +225,8 @@ public record HandlerOpenTaskDto(string Id, string Title);
|
|||||||
|
|
||||||
public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet, string? FallbackListId = null);
|
public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet, string? FallbackListId = null);
|
||||||
public record TicketConnectionDto(bool Ok, string? UserName, IReadOnlyList<string> Scopes, string? Error);
|
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 TicketProjectDto(int Id, string Title, string DepartmentName);
|
||||||
public record TicketImportResultDto(int Examined, int Created);
|
public record TicketImportResultDto(int Examined, int Created);
|
||||||
|
|||||||
@@ -252,6 +252,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task SetTicketTokenAsync(string token);
|
Task SetTicketTokenAsync(string token);
|
||||||
Task ClearTicketTokenAsync();
|
Task ClearTicketTokenAsync();
|
||||||
Task<TicketConnectionDto?> TestTicketConnectionAsync();
|
Task<TicketConnectionDto?> TestTicketConnectionAsync();
|
||||||
|
Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync();
|
||||||
|
Task SetAzureDevOpsTokenAsync(string token);
|
||||||
|
Task ClearAzureDevOpsTokenAsync();
|
||||||
|
Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync();
|
||||||
Task<List<TicketProjectDto>> GetTicketProjectsAsync();
|
Task<List<TicketProjectDto>> GetTicketProjectsAsync();
|
||||||
/// <summary>Imports open tickets assigned to the token owner into the given list. Throws
|
/// <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
|
/// HubException (with a user-readable message) on failure so the caller can surface it via
|
||||||
|
|||||||
@@ -776,6 +776,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public Task<TicketConnectionDto?> TestTicketConnectionAsync()
|
public Task<TicketConnectionDto?> TestTicketConnectionAsync()
|
||||||
=> TryInvokeAsync<TicketConnectionDto>("TestTicketConnection");
|
=> 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()
|
public async Task<List<TicketProjectDto>> GetTicketProjectsAsync()
|
||||||
=> await TryInvokeAsync<List<TicketProjectDto>>("GetTicketProjects") ?? [];
|
=> await TryInvokeAsync<List<TicketProjectDto>>("GetTicketProjects") ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using ClaudeDo.Worker.Report;
|
|||||||
using ClaudeDo.Worker.Report.Interfaces;
|
using ClaudeDo.Worker.Report.Interfaces;
|
||||||
using ClaudeDo.Worker.Skills;
|
using ClaudeDo.Worker.Skills;
|
||||||
using ClaudeDo.Worker.State;
|
using ClaudeDo.Worker.State;
|
||||||
|
using ClaudeDo.Worker.AzureDevOps;
|
||||||
using ClaudeDo.Worker.Tickets;
|
using ClaudeDo.Worker.Tickets;
|
||||||
using ClaudeDo.Worker.Usage;
|
using ClaudeDo.Worker.Usage;
|
||||||
using ClaudeDo.Worker.Usage.Interfaces;
|
using ClaudeDo.Worker.Usage.Interfaces;
|
||||||
@@ -75,6 +76,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly HandlerRestartCoordinator? _handlerRestart;
|
private readonly HandlerRestartCoordinator? _handlerRestart;
|
||||||
private readonly HandlerRoundState? _roundState;
|
private readonly HandlerRoundState? _roundState;
|
||||||
private readonly ConnectedClientTracker? _clientTracker;
|
private readonly ConnectedClientTracker? _clientTracker;
|
||||||
|
private readonly AzureDevOpsPatStore? _azdoPat;
|
||||||
|
private readonly AzureDevOpsClient? _azdo;
|
||||||
|
|
||||||
public WorkerHub(
|
public WorkerHub(
|
||||||
QueueService queue,
|
QueueService queue,
|
||||||
@@ -116,7 +119,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
TicketImportService ticketImport = null!,
|
TicketImportService ticketImport = null!,
|
||||||
HandlerRestartCoordinator? handlerRestart = null,
|
HandlerRestartCoordinator? handlerRestart = null,
|
||||||
HandlerRoundState? roundState = null,
|
HandlerRoundState? roundState = null,
|
||||||
ConnectedClientTracker? clientTracker = null)
|
ConnectedClientTracker? clientTracker = null,
|
||||||
|
AzureDevOpsPatStore? azdoPat = null,
|
||||||
|
AzureDevOpsClient? azdo = null)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_waker = waker;
|
_waker = waker;
|
||||||
@@ -158,6 +163,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_handlerRestart = handlerRestart;
|
_handlerRestart = handlerRestart;
|
||||||
_roundState = roundState;
|
_roundState = roundState;
|
||||||
_clientTracker = clientTracker;
|
_clientTracker = clientTracker;
|
||||||
|
_azdoPat = azdoPat;
|
||||||
|
_azdo = azdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracks connected UI clients so an out-of-band caller (external MCP's start_list_handler)
|
// 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()
|
public async Task<List<TicketProjectDto>> GetTicketProjects()
|
||||||
{
|
{
|
||||||
var client = _ticketClients.Create();
|
var client = _ticketClients.Create();
|
||||||
|
|||||||
@@ -227,6 +227,10 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task SetTicketTokenAsync(string token) => Task.CompletedTask;
|
public virtual Task SetTicketTokenAsync(string token) => Task.CompletedTask;
|
||||||
public virtual Task ClearTicketTokenAsync() => Task.CompletedTask;
|
public virtual Task ClearTicketTokenAsync() => Task.CompletedTask;
|
||||||
public virtual Task<TicketConnectionDto?> TestTicketConnectionAsync() => Task.FromResult<TicketConnectionDto?>(null);
|
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<List<TicketProjectDto>> GetTicketProjectsAsync() => Task.FromResult(new List<TicketProjectDto>());
|
||||||
public virtual Task<TicketImportResultDto> ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));
|
public virtual Task<TicketImportResultDto> ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user