From 2a3c937f70174f902f80d78efc2b6ae2c5a47135 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 27 Aug 2026 12:34:24 +0200 Subject: [PATCH] feat(worker): TicketSystemConfig + ticket_api_base_url in worker.config.json --- src/ClaudeDo.Worker/Config/WorkerConfig.cs | 9 ++++ src/ClaudeDo.Worker/Program.cs | 2 + .../Tickets/TicketSystemConfig.cs | 29 +++++++++++++ .../Tickets/TicketSystemConfigTests.cs | 43 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/ClaudeDo.Worker/Tickets/TicketSystemConfig.cs create mode 100644 tests/ClaudeDo.Worker.Tests/Tickets/TicketSystemConfigTests.cs diff --git a/src/ClaudeDo.Worker/Config/WorkerConfig.cs b/src/ClaudeDo.Worker/Config/WorkerConfig.cs index 5a8a2026..f5f7026d 100644 --- a/src/ClaudeDo.Worker/Config/WorkerConfig.cs +++ b/src/ClaudeDo.Worker/Config/WorkerConfig.cs @@ -57,6 +57,10 @@ public sealed class WorkerConfig [JsonPropertyName("usage_poll_interval_idle_seconds")] public int UsagePollIntervalIdleSeconds { get; set; } = 900; + /// Base URL of the Bandel ticket-system API. Null = ticket integration off entirely. + [JsonPropertyName("ticket_api_base_url")] + public string? TicketApiBaseUrl { get; set; } + public static string DefaultConfigPath => Path.Combine(Paths.AppDataRoot(), "worker.config.json"); @@ -103,6 +107,11 @@ public sealed class WorkerConfig public void SaveClaudeBin(string? path = null) => SaveKey("claude_bin", JsonValue.Create(ClaudeBin), path); + /// Persistiert NUR ticket_api_base_url, gleiches read-modify-write wie + /// — jedes andere Feld der Datei bleibt unangetastet. + public void SaveTicketApiBaseUrl(string? path = null) + => SaveKey("ticket_api_base_url", JsonValue.Create(TicketApiBaseUrl), path); + private static void SaveKey(string key, JsonNode? value, string? path) { path ??= DefaultConfigPath; diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 5126053c..64ce20e8 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -15,6 +15,7 @@ using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.State; +using ClaudeDo.Worker.Tickets; using ClaudeDo.Worker.Online; using ClaudeDo.Worker.Online.Interfaces; using ClaudeDo.Worker.Prime; @@ -215,6 +216,7 @@ builder.Services.AddSingleton(cfg.OnlineInbox); builder.Services.AddSingleton(new OnlineRefreshTokenStore(DpapiTokenStore.InAppData("online-inbox.token"))); builder.Services.AddSingleton(new TicketPatStore(DpapiTokenStore.InAppData("ticket.pat"))); #pragma warning restore CA1416 +builder.Services.AddSingleton(); if (cfg.OnlineInbox.Enabled) { diff --git a/src/ClaudeDo.Worker/Tickets/TicketSystemConfig.cs b/src/ClaudeDo.Worker/Tickets/TicketSystemConfig.cs new file mode 100644 index 00000000..5d737df7 --- /dev/null +++ b/src/ClaudeDo.Worker/Tickets/TicketSystemConfig.cs @@ -0,0 +1,29 @@ +using ClaudeDo.Worker.Config; + +namespace ClaudeDo.Worker.Tickets; + +/// +/// Der eine Ort, der beantwortet "ist die Ticketsystem-Anbindung eingerichtet?". +/// Liest die Base-URL aus dem WorkerConfig-Singleton (in-place aktualisiert vom Hub-Setter, +/// also ohne Neustart wirksam) und den PAT aus dem DPAPI-Store. Beide fehlen im Normalfall, +/// deshalb ist IsConfigured die billigste denkbare Abbruchbedingung: kein IO, keine DB. +/// +public sealed class TicketSystemConfig +{ + private readonly WorkerConfig _worker; + private readonly TicketPatStore _pat; + + public TicketSystemConfig(WorkerConfig worker, TicketPatStore pat) + { + _worker = worker; + _pat = pat; + } + + public string? BaseUrl => string.IsNullOrWhiteSpace(_worker.TicketApiBaseUrl) + ? null + : _worker.TicketApiBaseUrl.Trim().TrimEnd('/'); + + public string? Token => _pat.Store.Read(); + + public bool IsConfigured => BaseUrl is not null && !string.IsNullOrEmpty(Token); +} diff --git a/tests/ClaudeDo.Worker.Tests/Tickets/TicketSystemConfigTests.cs b/tests/ClaudeDo.Worker.Tests/Tickets/TicketSystemConfigTests.cs new file mode 100644 index 00000000..dfba78fb --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Tickets/TicketSystemConfigTests.cs @@ -0,0 +1,43 @@ +using ClaudeDo.Worker.Config; +using ClaudeDo.Worker.Tickets; + +namespace ClaudeDo.Worker.Tests.Tickets; + +public sealed class TicketSystemConfigTests : IDisposable +{ + private readonly string _dir = Path.Combine(Path.GetTempPath(), "cdo-tcfg-" + Guid.NewGuid().ToString("N")); + + private TicketSystemConfig Make(string? baseUrl, string? pat) + { + Directory.CreateDirectory(_dir); + var store = new DpapiTokenStore(Path.Combine(_dir, "ticket.pat")); + if (pat is not null) store.Save(pat); + var worker = new WorkerConfig { TicketApiBaseUrl = baseUrl }; + return new TicketSystemConfig(worker, new TicketPatStore(store)); + } + + [Fact] + public void Not_configured_without_base_url() + => Assert.False(Make(null, "tsp_x").IsConfigured); + + [Fact] + public void Not_configured_without_pat() + => Assert.False(Make("http://api.local", null).IsConfigured); + + [Fact] + public void Configured_with_both() + => Assert.True(Make("http://api.local", "tsp_x").IsConfigured); + + [Fact] + public void Blank_base_url_counts_as_missing() + => Assert.False(Make(" ", "tsp_x").IsConfigured); + + [Fact] + public void Base_url_trailing_slash_is_trimmed() + => Assert.Equal("http://api.local", Make("http://api.local/", "tsp_x").BaseUrl); + + public void Dispose() + { + if (Directory.Exists(_dir)) Directory.Delete(_dir, recursive: true); + } +}