feat(hub): Ticket-Settings, Projektliste und Import ueber den Hub

This commit is contained in:
mika kuns
2026-08-27 13:22:54 +02:00
parent 2e236736a7
commit f0a3a186dd
9 changed files with 255 additions and 10 deletions
+5 -2
View File
@@ -106,10 +106,12 @@ public sealed class ConfigMcpTools
// Fields this tool doesn't expose but that live on the same row. They must survive every
// write here — ListRepository.SetConfigAsync copies the entity verbatim, so anything left
// at its default would silently reset (a SerializeOnFileOverlap reset only shows up as
// tasks no longer serializing, long after this write).
// tasks no longer serializing, long after this write; a TicketProjectId reset silently
// kills the list<->ticket-project link — the tool has no ticket parameter on purpose,
// that link is UI-only).
var hasUnrelatedSettings = existing is not null
&& (existing.SessionSkills is not null || existing.SerializeOnFileOverlap
|| existing.PermissionMode is not null);
|| existing.PermissionMode is not null || existing.TicketProjectId is not null);
ListConfigDto? config;
var allCleared = m is null && sp is null && ap is null && mt is null && vc is null;
@@ -130,6 +132,7 @@ public sealed class ConfigMcpTools
VerifyCommand = vc, SessionSkills = existing?.SessionSkills,
SerializeOnFileOverlap = existing?.SerializeOnFileOverlap ?? false,
PermissionMode = existing?.PermissionMode,
TicketProjectId = existing?.TicketProjectId,
}, cancellationToken);
config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc);
}
+82 -3
View File
@@ -18,6 +18,7 @@ using ClaudeDo.Worker.Report;
using ClaudeDo.Worker.Report.Interfaces;
using ClaudeDo.Worker.Skills;
using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Tickets;
using ClaudeDo.Worker.Usage;
using ClaudeDo.Worker.Usage.Interfaces;
using ClaudeDo.Worker.Usage.TokenTracker;
@@ -81,6 +82,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
private readonly UsageMonitorService? _usageMonitor;
private readonly TokenTrackerService? _tokenTracker;
private readonly InteractiveReviewSubmissionService? _interactiveReviewSubmission;
private readonly TicketSystemConfig _ticketConfig;
private readonly TicketPatStore _ticketPat;
private readonly TicketClientFactory _ticketClients;
private readonly TicketImportService _ticketImport;
public WorkerHub(
QueueService queue,
@@ -115,7 +120,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
ITranscriptUsageReader? usageReader = null,
UsageMonitorService? usageMonitor = null,
InteractiveReviewSubmissionService? interactiveReviewSubmission = null,
TokenTrackerService? tokenTracker = null)
TokenTrackerService? tokenTracker = null,
TicketSystemConfig ticketConfig = null!,
TicketPatStore ticketPat = null!,
TicketClientFactory ticketClients = null!,
TicketImportService ticketImport = null!)
{
_queue = queue;
_waker = waker;
@@ -150,6 +159,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
_usageMonitor = usageMonitor;
_interactiveReviewSubmission = interactiveReviewSubmission;
_tokenTracker = tokenTracker;
_ticketConfig = ticketConfig;
_ticketPat = ticketPat;
_ticketClients = ticketClients;
_ticketImport = ticketImport;
}
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
@@ -553,7 +566,17 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
var existing = await repo.GetConfigAsync(dto.ListId);
var serializeOnFileOverlap = dto.SerializeOnFileOverlap ?? existing?.SerializeOnFileOverlap ?? false;
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && !serializeOnFileOverlap)
// Gleiche Tri-State-Regel wie oben: null = gespeicherten Wert behalten, 0 = Verknüpfung
// löschen. Ohne das würde jeder fremde Writer (set_list_config MCP-Tool, Agent-Settings)
// die Ticket-Verknüpfung still kappen — SetConfigAsync kopiert verbatim.
var ticketProjectId = dto.TicketProjectId switch
{
null => existing?.TicketProjectId,
<= 0 => null,
var id => id,
};
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && !serializeOnFileOverlap && ticketProjectId is null)
{
await repo.DeleteConfigAsync(dto.ListId);
}
@@ -570,6 +593,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
VerifyCommand = verifyCommand,
SerializeOnFileOverlap = serializeOnFileOverlap,
PermissionMode = permissionMode,
TicketProjectId = ticketProjectId,
});
}
@@ -593,7 +617,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
var repo = new ListRepository(ctx);
var config = await repo.GetConfigAsync(listId);
if (config is null) return null;
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap, config.PermissionMode);
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap, config.PermissionMode, config.TicketProjectId);
}
public async Task<SetTaskStatusResultDto> SetTaskStatus(string taskId, string status)
@@ -1249,4 +1273,59 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
.Take(100)
.ToList();
}
public TicketSettingsDto GetTicketSettings()
=> new(_cfg.TicketApiBaseUrl, _ticketConfig.Token is not null);
public void SetTicketApiBaseUrl(string? baseUrl)
{
_cfg.TicketApiBaseUrl = string.IsNullOrWhiteSpace(baseUrl) ? null : baseUrl.Trim();
_cfg.SaveTicketApiBaseUrl();
}
public void SetTicketToken(string token)
{
if (string.IsNullOrWhiteSpace(token)) _ticketPat.Store.Clear();
else _ticketPat.Store.Save(token.Trim());
}
public void ClearTicketToken() => _ticketPat.Store.Clear();
public async Task<TicketConnectionDto> TestTicketConnection()
{
var client = _ticketClients.Create();
if (client is null)
return new TicketConnectionDto(false, null, Array.Empty<string>(), "Base-URL oder Token fehlt.");
try
{
var identity = await client.GetIdentityAsync(Context.ConnectionAborted);
return new TicketConnectionDto(true, identity.UserName, identity.Scopes, null);
}
catch (TicketApiException ex)
{
return new TicketConnectionDto(false, null, Array.Empty<string>(), ex.Message);
}
}
public async Task<List<TicketProjectDto>> GetTicketProjects()
{
var client = _ticketClients.Create();
if (client is null) return new List<TicketProjectDto>();
var projects = await client.GetProjectsAsync(Context.ConnectionAborted);
return projects.Select(p => new TicketProjectDto(p.Id, p.Title, p.DepartmentName)).ToList();
}
public async Task<TicketImportResultDto> ImportTickets(string listId)
{
try
{
var result = await _ticketImport.ImportAsync(listId, Context.ConnectionAborted);
await _broadcaster.ListUpdated(listId);
return new TicketImportResultDto(result.Examined, result.Created);
}
catch (TicketApiException ex)
{
throw new HubException(ex.Message);
}
}
}