diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index d062644e..824b95ff 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -14,7 +14,8 @@ "categoryWorktrees": "Worktrees", "categoryFiles": "Dateien & Prompts", "categorySkills": "Session Skills", - "categoryReports": "Berichte" + "categoryReports": "Berichte", + "categoryTickets": "Ticketsystem" }, "repoHint": { "text": "Wirkt erst mit verknüpftem Repo.", @@ -132,6 +133,14 @@ "configSection": "KONFIGURATION", "saveButton": "Konfiguration speichern" }, + "tickets": { + "baseUrl": "API-Base-URL", + "baseUrlHint": "Leer lassen, um die Ticket-Anbindung komplett zu deaktivieren.", + "token": "Personal Access Token", + "tokenSet": "gespeichert", + "testConnection": "Verbindung testen", + "clearToken": "Token entfernen" + }, "inherit": { "inheritedFromList": "geerbt · Liste", "inheritedFromGlobal": "geerbt · Global", @@ -726,6 +735,7 @@ "merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" }, "settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" }, "onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" }, + "tickets": { "connectionOk": "Verbunden als {0}.", "connectionMissingScopes": "Verbunden als {0}, aber es fehlen Scopes: {1}", "tokenCleared": "Token entfernt.", "workerOffline": "Worker nicht erreichbar." }, "weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "error": "Fehler: {0}" }, "usageMonitor": { "loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index eeac0fac..6ae389b7 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -14,7 +14,8 @@ "categoryWorktrees": "Worktrees", "categoryFiles": "Files & Prompts", "categorySkills": "Session Skills", - "categoryReports": "Reports" + "categoryReports": "Reports", + "categoryTickets": "Ticket system" }, "repoHint": { "text": "Only takes effect once a repo is linked.", @@ -132,6 +133,14 @@ "configSection": "CONFIGURATION", "saveButton": "Save config" }, + "tickets": { + "baseUrl": "API base URL", + "baseUrlHint": "Leave empty to disable the ticket integration entirely.", + "token": "Personal access token", + "tokenSet": "stored", + "testConnection": "Test connection", + "clearToken": "Remove token" + }, "inherit": { "inheritedFromList": "inherited · List", "inheritedFromGlobal": "inherited · Global", @@ -726,6 +735,7 @@ "merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" }, "settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" }, "onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" }, + "tickets": { "connectionOk": "Connected as {0}.", "connectionMissingScopes": "Connected as {0}, but missing scopes: {1}", "tokenCleared": "Token removed.", "workerOffline": "Worker not reachable." }, "weeklyReport": { "invalidRange": "Invalid date range.", "error": "Error: {0}" }, "usageMonitor": { "loadFailed": "Couldn't load usage data: {0}", diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs new file mode 100644 index 00000000..7d6915ba --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs @@ -0,0 +1,92 @@ +using ClaudeDo.Ui.Localization; +using ClaudeDo.Ui.Services; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Modals.Settings; + +public sealed partial class TicketSettingsTabViewModel : ViewModelBase +{ + private readonly IWorkerClient _worker; + + [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") : ""; + + public TicketSettingsTabViewModel(IWorkerClient worker) => _worker = worker; + + public async Task LoadAsync() + { + var dto = await _worker.GetTicketSettingsAsync(); + if (dto is null) + { + StatusMessage = Loc.T("vm.tickets.workerOffline"); + return; + } + ApiBaseUrl = dto.ApiBaseUrl ?? ""; + TokenSet = dto.TokenSet; + } + + public async Task SaveAsync() + { + await _worker.SetTicketApiBaseUrlAsync(ApiBaseUrl); + if (!string.IsNullOrWhiteSpace(Token)) + { + await _worker.SetTicketTokenAsync(Token); + Token = ""; + TokenSet = true; + } + } + + [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", + ]; +} diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index f47a7f7c..5f8ca183 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -26,6 +26,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase public PrimeClaudeTabViewModel Prime { get; } public OnlineInboxSettingsViewModel OnlineInbox { get; } public SessionSkillsSettingsTabViewModel SessionSkills { get; } + public TicketSettingsTabViewModel Tickets { get; } // Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung. public bool ShowOnlineInbox => false; @@ -106,6 +107,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase Prime = prime; OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService); SessionSkills = new SessionSkillsSettingsTabViewModel(worker); + Tickets = new TicketSettingsTabViewModel(worker); _categories = new List { @@ -117,6 +119,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase new("SessionSkills", new LocalizedString(localizer, "settings.sidebar.categorySkills"), "Erweitert", true, true), new("Reports", new LocalizedString(localizer, "settings.sidebar.categoryReports"), "Erweitert", true, true), new("OnlineInbox", new LocalizedString(localizer, "settings.onlineInbox.tabHeader"), "Erweitert", ShowOnlineInbox, false), + new("Tickets", new LocalizedString(localizer, "settings.sidebar.categoryTickets"), "Erweitert", true, false), }; BasisCategories = _categories.Where(c => c.Group == "Basis").ToList(); ErweitertCategories = _categories.Where(c => c.Group == "Erweitert").ToList(); @@ -167,6 +170,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase await Prime.LoadAsync(); await OnlineInbox.LoadAsync(); await SessionSkills.LoadAsync(dto?.SessionSkills); + await Tickets.LoadAsync(); General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling); await using var ctx = await _dbFactory.CreateDbContextAsync(); @@ -213,6 +217,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase await _worker.UpdateAppSettingsAsync(dto); await Prime.SaveAsync(); await OnlineInbox.SaveAsync(); + await Tickets.SaveAsync(); CloseAction?.Invoke(); } catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); } diff --git a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml index 8b96ed38..950c18d0 100644 --- a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml @@ -690,6 +690,45 @@ + + + + + + + + + + + + + + + + + + + +