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);
+ }
+}