Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs
T

180 lines
6.4 KiB
C#

using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
/// <summary>One fallback-list choice; Id null = no fallback.</summary>
public sealed record FallbackListOption(string? Id, string Name);
public sealed partial class TicketSettingsTabViewModel : ViewModelBase
{
/// <summary>
/// Der Aus-Schalter der gesamten Ticketsystem-Anbindung in der UI, wie bei
/// <c>SettingsModalViewModel.ShowOnlineInbox</c>. Er versteckt drei Einstiegspunkte: diesen
/// Settings-Tab, das Ticket-Projekt-Dropdown in den Listen-Einstellungen und den
/// Kontextmenü-Eintrag zum Importieren.
///
/// Pausiert am 2026-08-27 (API unter dem Hostnamen nicht erreichbar), reaktiviert am
/// 2026-09-01: die API läuft unter <c>http://192.168.45.161</c> und alle vier Routen sind
/// live verifiziert. Base-URL ist deshalb eine IP, kein Hostname — <c>bandelapis.fb-tuning.local</c>
/// lehnt die Verbindung weiterhin ab.
/// </summary>
public const bool FeatureEnabled = true;
private readonly IWorkerClient _worker;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
[ObservableProperty] private string _apiBaseUrl = "";
[ObservableProperty] private string _token = "";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TokenPlaceholder))]
private bool _tokenSet;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string _statusMessage = "";
// Never shows the stored token — TicketSettingsDto only ever carries the bool.
public string TokenPlaceholder => TokenSet ? Loc.T("settings.tickets.tokenSet") : "";
/// <summary>Azure DevOps PAT being typed; never shows the stored one.</summary>
[ObservableProperty] private string _azdoToken = "";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AzdoTokenPlaceholder))]
private bool _azdoTokenSet;
[ObservableProperty] private string _azdoStatusMessage = "";
public string AzdoTokenPlaceholder => AzdoTokenSet ? Loc.T("settings.tickets.tokenSet") : "";
public ObservableCollection<FallbackListOption> FallbackLists { get; } = new();
[ObservableProperty] private FallbackListOption? _selectedFallbackList;
// Only a successful load may write the fallback back — otherwise an offline load would clear it.
private bool _fallbackLoaded;
public TicketSettingsTabViewModel(IWorkerClient worker, IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
_worker = worker;
_dbFactory = dbFactory;
}
public async Task LoadAsync()
{
AzdoTokenSet = (await _worker.GetAzureDevOpsSettingsAsync())?.TokenSet ?? false;
var dto = await _worker.GetTicketSettingsAsync();
if (dto is null)
{
StatusMessage = Loc.T("vm.tickets.workerOffline");
return;
}
ApiBaseUrl = dto.ApiBaseUrl ?? "";
TokenSet = dto.TokenSet;
await using var ctx = await _dbFactory.CreateDbContextAsync();
var lists = await ctx.Lists.AsNoTracking()
.OrderBy(l => l.SortOrder).ThenBy(l => l.Name)
.Select(l => new FallbackListOption(l.Id, l.Name))
.ToListAsync();
FallbackLists.Clear();
FallbackLists.Add(new FallbackListOption(null, Loc.T("settings.tickets.fallbackNone")));
foreach (var option in lists) FallbackLists.Add(option);
SelectedFallbackList = FallbackLists.FirstOrDefault(o => o.Id == dto.FallbackListId) ?? FallbackLists[0];
_fallbackLoaded = true;
}
public async Task SaveAsync()
{
await _worker.SetTicketApiBaseUrlAsync(ApiBaseUrl);
if (!string.IsNullOrWhiteSpace(Token))
{
await _worker.SetTicketTokenAsync(Token);
Token = "";
TokenSet = true;
}
if (_fallbackLoaded)
await _worker.SetTicketFallbackListAsync(SelectedFallbackList?.Id);
if (!string.IsNullOrWhiteSpace(AzdoToken))
{
await _worker.SetAzureDevOpsTokenAsync(AzdoToken);
AzdoToken = "";
AzdoTokenSet = true;
}
}
[RelayCommand]
private async Task TestAzdoConnection()
{
IsBusy = true;
AzdoStatusMessage = "";
try
{
await SaveAsync();
var result = await _worker.TestAzureDevOpsConnectionAsync();
AzdoStatusMessage = result is { Ok: true }
? Loc.T("vm.tickets.azdoConnectionOk", result.Repo!, result.DefaultBranch!)
: result?.Error ?? Loc.T("vm.tickets.workerOffline");
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task ClearAzdoToken()
{
await _worker.ClearAzureDevOpsTokenAsync();
AzdoToken = "";
AzdoTokenSet = false;
AzdoStatusMessage = Loc.T("vm.tickets.tokenCleared");
}
[RelayCommand]
private async Task TestConnection()
{
IsBusy = true;
StatusMessage = "";
try
{
await SaveAsync();
var result = await _worker.TestTicketConnectionAsync();
if (result is null || !result.Ok)
{
StatusMessage = result?.Error ?? Loc.T("vm.tickets.workerOffline");
return;
}
var missing = RequiredScopes.Where(s => !result.Scopes.Contains(s)).ToList();
StatusMessage = missing.Count == 0
? Loc.T("vm.tickets.connectionOk", result.UserName!)
: Loc.T("vm.tickets.connectionMissingScopes", result.UserName!, string.Join(", ", missing));
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task ClearToken()
{
await _worker.ClearTicketTokenAsync();
Token = "";
TokenSet = false;
StatusMessage = Loc.T("vm.tickets.tokenCleared");
}
// Ohne diese drei Scopes scheitert entweder der Import oder die Statusrueckmeldung —
// der mit Abstand haeufigste Konfigurationsfehler, deshalb wird er beim Testen benannt
// statt nur geloggt.
private static readonly string[] RequiredScopes =
[
"pat:projects:read",
"pat:board:read",
"pat:tickets:write",
];
}