feat: Parent-Child TaskFlow — Delegated durch sichtbare Child-Tasks ersetzt
- Delegated State aus Board, Entities, DTOs, Frontend-Spalten und Tests entfernt - Parent-Tasks bleiben InProgress waehrend delegierter Agentenarbeit - Child-Tasks laufen sichtbar mit normalen States und parentTaskId - Doku: README, Phase 3, Changelog, Controller-Kommentare angepasst - openclaw-task-board-flow.md als Referenzdoku hinzugefuegt - 73/73 Backend-Tests gruen, Frontend-Build gruen
This commit is contained in:
@@ -163,7 +163,7 @@ Legacy ModuleView routes (not standalone, rendered through `ModuleView.vue`):
|
|||||||
| Route | Name | Description |
|
| Route | Name | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/projects` | Projects | Project portfolio |
|
| `/projects` | Projects | Project portfolio |
|
||||||
| `/tasks` | Task Board | Task board |
|
| `/tasks` | Task Board | Task board with visible parent/child agent flow |
|
||||||
| `/models` | Models | Provider routing status |
|
| `/models` | Models | Provider routing status |
|
||||||
| `/activity` | Activity | Audit timeline |
|
| `/activity` | Activity | Audit timeline |
|
||||||
| `/chat` | Mobile Chat | Owner-chat preview |
|
| `/chat` | Mobile Chat | Owner-chat preview |
|
||||||
@@ -189,6 +189,15 @@ Legacy ModuleView routes (not standalone, rendered through `ModuleView.vue`):
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GET` | `/api/v1/operations/snapshot` | Full operations snapshot (runtime, agents, projects, tasks, activity, metrics) |
|
| `GET` | `/api/v1/operations/snapshot` | Full operations snapshot (runtime, agents, projects, tasks, activity, metrics) |
|
||||||
|
|
||||||
|
### Parent/Child task flow
|
||||||
|
|
||||||
|
The Task Board now models OpenClaw delegation as a visible parent/child flow:
|
||||||
|
- Iris keeps the parent task `In progress` while delegated work is running.
|
||||||
|
- Delegated agent work is represented as visible child tasks linked via `parentTaskId`.
|
||||||
|
- Child tasks use the normal visible states (`Backlog`, `In progress`, `Review`, `Blocked`, `Done`) instead of a separate hidden delegation lane.
|
||||||
|
- Agent progress hints on parent tasks derive from recent activity and child-task status summaries.
|
||||||
|
- Full workflow documentation: [`docs/openclaw-task-board-flow.md`](docs/openclaw-task-board-flow.md)
|
||||||
|
|
||||||
### Projects
|
### Projects
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ public class TaskBoardTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Backlog", "offen")]
|
[InlineData("Backlog", "offen")]
|
||||||
[InlineData("In progress", "inProgress")]
|
[InlineData("In progress", "inProgress")]
|
||||||
[InlineData("Delegated", "delegated")]
|
|
||||||
[InlineData("Review", "review")]
|
[InlineData("Review", "review")]
|
||||||
[InlineData("Blocked", "blocked")]
|
[InlineData("Blocked", "blocked")]
|
||||||
[InlineData("Done", "done")]
|
[InlineData("Done", "done")]
|
||||||
[InlineData("backlog", "offen")]
|
[InlineData("backlog", "offen")]
|
||||||
[InlineData("in progress", "inProgress")]
|
[InlineData("in progress", "inProgress")]
|
||||||
[InlineData("delegated", "delegated")]
|
|
||||||
[InlineData("review", "review")]
|
[InlineData("review", "review")]
|
||||||
[InlineData("blocked", "blocked")]
|
[InlineData("blocked", "blocked")]
|
||||||
[InlineData("done", "done")]
|
[InlineData("done", "done")]
|
||||||
@@ -35,7 +33,6 @@ public class TaskBoardTests
|
|||||||
[InlineData("offen", "Backlog")]
|
[InlineData("offen", "Backlog")]
|
||||||
[InlineData("inProgress", "In progress")]
|
[InlineData("inProgress", "In progress")]
|
||||||
[InlineData("inprogress", "In progress")]
|
[InlineData("inprogress", "In progress")]
|
||||||
[InlineData("delegated", "Delegated")]
|
|
||||||
[InlineData("review", "Review")]
|
[InlineData("review", "Review")]
|
||||||
[InlineData("blocked", "Blocked")]
|
[InlineData("blocked", "Blocked")]
|
||||||
[InlineData("done", "Done")]
|
[InlineData("done", "Done")]
|
||||||
@@ -49,16 +46,15 @@ public class TaskBoardTests
|
|||||||
Assert.Equal(expected, result);
|
Assert.Equal(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TaskStateHelper: AllStates has 6 entries ──
|
// ── TaskStateHelper: AllStates has 5 entries ──
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AllStates_ContainsAllSixStates()
|
public void AllStates_ContainsAllFiveStates()
|
||||||
{
|
{
|
||||||
var states = TaskStateHelper.AllStates;
|
var states = TaskStateHelper.AllStates;
|
||||||
Assert.Equal(6, states.Length);
|
Assert.Equal(5, states.Length);
|
||||||
Assert.Contains("Backlog", states);
|
Assert.Contains("Backlog", states);
|
||||||
Assert.Contains("In progress", states);
|
Assert.Contains("In progress", states);
|
||||||
Assert.Contains("Delegated", states);
|
|
||||||
Assert.Contains("Review", states);
|
Assert.Contains("Review", states);
|
||||||
Assert.Contains("Blocked", states);
|
Assert.Contains("Blocked", states);
|
||||||
Assert.Contains("Done", states);
|
Assert.Contains("Done", states);
|
||||||
@@ -69,7 +65,6 @@ public class TaskBoardTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Backlog", true)]
|
[InlineData("Backlog", true)]
|
||||||
[InlineData("In progress", true)]
|
[InlineData("In progress", true)]
|
||||||
[InlineData("Delegated", true)]
|
|
||||||
[InlineData("Review", true)]
|
[InlineData("Review", true)]
|
||||||
[InlineData("Blocked", true)]
|
[InlineData("Blocked", true)]
|
||||||
[InlineData("Done", true)]
|
[InlineData("Done", true)]
|
||||||
@@ -89,7 +84,6 @@ public class TaskBoardTests
|
|||||||
[InlineData("In progress", true)]
|
[InlineData("In progress", true)]
|
||||||
[InlineData("Blocked", true)]
|
[InlineData("Blocked", true)]
|
||||||
[InlineData("Backlog", false)]
|
[InlineData("Backlog", false)]
|
||||||
[InlineData("Delegated", false)]
|
|
||||||
[InlineData("Review", false)]
|
[InlineData("Review", false)]
|
||||||
[InlineData("Done", false)]
|
[InlineData("Done", false)]
|
||||||
[InlineData(null, false)]
|
[InlineData(null, false)]
|
||||||
@@ -104,7 +98,6 @@ public class TaskBoardTests
|
|||||||
[InlineData("Done", true)]
|
[InlineData("Done", true)]
|
||||||
[InlineData("Backlog", true)]
|
[InlineData("Backlog", true)]
|
||||||
[InlineData("In progress", false)]
|
[InlineData("In progress", false)]
|
||||||
[InlineData("Delegated", false)]
|
|
||||||
[InlineData("Review", false)]
|
[InlineData("Review", false)]
|
||||||
[InlineData("Blocked", false)]
|
[InlineData("Blocked", false)]
|
||||||
[InlineData(null, false)]
|
[InlineData(null, false)]
|
||||||
@@ -118,7 +111,6 @@ public class TaskBoardTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Backlog", "Offen")]
|
[InlineData("Backlog", "Offen")]
|
||||||
[InlineData("In progress", "In Bearbeitung")]
|
[InlineData("In progress", "In Bearbeitung")]
|
||||||
[InlineData("Delegated", "Delegiert")]
|
|
||||||
[InlineData("Review", "Review")]
|
[InlineData("Review", "Review")]
|
||||||
[InlineData("Blocked", "Blockiert")]
|
[InlineData("Blocked", "Blockiert")]
|
||||||
[InlineData("Done", "Erledigt")]
|
[InlineData("Done", "Erledigt")]
|
||||||
@@ -136,7 +128,7 @@ public class TaskBoardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToStateString_And_ToTaskState_RoundTrip()
|
public void ToStateString_And_ToTaskState_RoundTrip()
|
||||||
{
|
{
|
||||||
var states = new[] { TaskState.Backlog, TaskState.InProgress, TaskState.Delegated, TaskState.Review, TaskState.Blocked, TaskState.Done };
|
var states = new[] { TaskState.Backlog, TaskState.InProgress, TaskState.Review, TaskState.Blocked, TaskState.Done };
|
||||||
foreach (var state in states)
|
foreach (var state in states)
|
||||||
{
|
{
|
||||||
var str = state.ToStateString();
|
var str = state.ToStateString();
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public class TasksController(ITaskService taskService) : ControllerBase
|
|||||||
=> Results.Ok(await taskService.GetBoardAsync(ct));
|
=> Results.Ok(await taskService.GetBoardAsync(ct));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setzt stale Tasks (InProgress/Delegated, älter als N Stunden) zurück auf Backlog.
|
/// Setzt stale Tasks (InProgress, älter als N Stunden) zurück auf Backlog.
|
||||||
/// Wird vom Iris Autonomous Worker genutzt.
|
/// Wird vom Iris Autonomous Worker genutzt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ public enum TaskState
|
|||||||
{
|
{
|
||||||
Backlog,
|
Backlog,
|
||||||
InProgress,
|
InProgress,
|
||||||
Delegated,
|
|
||||||
Blocked,
|
Blocked,
|
||||||
Done,
|
Done,
|
||||||
Review
|
Review
|
||||||
@@ -30,7 +29,6 @@ public static class TaskStateHelper
|
|||||||
{
|
{
|
||||||
[TaskState.Backlog] = "Backlog",
|
[TaskState.Backlog] = "Backlog",
|
||||||
[TaskState.InProgress] = "In progress",
|
[TaskState.InProgress] = "In progress",
|
||||||
[TaskState.Delegated] = "Delegated",
|
|
||||||
[TaskState.Blocked] = "Blocked",
|
[TaskState.Blocked] = "Blocked",
|
||||||
[TaskState.Done] = "Done",
|
[TaskState.Done] = "Done",
|
||||||
[TaskState.Review] = "Review"
|
[TaskState.Review] = "Review"
|
||||||
@@ -40,7 +38,6 @@ public static class TaskStateHelper
|
|||||||
{
|
{
|
||||||
["Backlog"] = TaskState.Backlog,
|
["Backlog"] = TaskState.Backlog,
|
||||||
["In progress"] = TaskState.InProgress,
|
["In progress"] = TaskState.InProgress,
|
||||||
["Delegated"] = TaskState.Delegated,
|
|
||||||
["Blocked"] = TaskState.Blocked,
|
["Blocked"] = TaskState.Blocked,
|
||||||
["Done"] = TaskState.Done,
|
["Done"] = TaskState.Done,
|
||||||
["Review"] = TaskState.Review
|
["Review"] = TaskState.Review
|
||||||
@@ -51,14 +48,13 @@ public static class TaskStateHelper
|
|||||||
{
|
{
|
||||||
["Backlog"] = "Offen",
|
["Backlog"] = "Offen",
|
||||||
["In progress"] = "In Bearbeitung",
|
["In progress"] = "In Bearbeitung",
|
||||||
["Delegated"] = "Delegiert",
|
|
||||||
["Review"] = "Review",
|
["Review"] = "Review",
|
||||||
["Blocked"] = "Blockiert",
|
["Blocked"] = "Blockiert",
|
||||||
["Done"] = "Erledigt"
|
["Done"] = "Erledigt"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Valid task-state string values for API validation.</summary>
|
/// <summary>Valid task-state string values for API validation.</summary>
|
||||||
public static readonly string[] AllStates = ["Backlog", "In progress", "Delegated", "Blocked", "Done", "Review"];
|
public static readonly string[] AllStates = ["Backlog", "In progress", "Blocked", "Done", "Review"];
|
||||||
|
|
||||||
/// <summary>Convert a TaskState enum to its API string representation.</summary>
|
/// <summary>Convert a TaskState enum to its API string representation.</summary>
|
||||||
public static string ToStateString(this TaskState state) => StateToString[state];
|
public static string ToStateString(this TaskState state) => StateToString[state];
|
||||||
@@ -87,7 +83,7 @@ public static class TaskStateHelper
|
|||||||
/// Returns true if the caller is allowed to change this task's state.
|
/// Returns true if the caller is allowed to change this task's state.
|
||||||
/// POLICY:
|
/// POLICY:
|
||||||
/// - **Iris und Bao** dürfen Status ändern / verschieben.
|
/// - **Iris und Bao** dürfen Status ändern / verschieben.
|
||||||
/// - Sub-agents (programmer, reviewer, architekt) dürfen NIEMALS Status ändern.
|
/// - Sub-agents (programmer, reviewer, architekt, researcher, executor) dürfen NIEMALS Status ändern.
|
||||||
/// - 'nexus-system' ist ein technischer Fallback für automatische Cron/Reset-Workflows.
|
/// - 'nexus-system' ist ein technischer Fallback für automatische Cron/Reset-Workflows.
|
||||||
/// - Jeder andere (unbekannt, leer) wird abgewiesen.
|
/// - Jeder andere (unbekannt, leer) wird abgewiesen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -96,7 +92,7 @@ public static class TaskStateHelper
|
|||||||
var caller = callerAgent?.Trim().ToLowerInvariant() ?? "";
|
var caller = callerAgent?.Trim().ToLowerInvariant() ?? "";
|
||||||
|
|
||||||
// Sub-agents must never move state
|
// Sub-agents must never move state
|
||||||
var subAgents = new HashSet<string> { "programmer", "reviewer", "architekt" };
|
var subAgents = new HashSet<string> { "programmer", "reviewer", "architekt", "researcher", "executor" };
|
||||||
if (subAgents.Contains(caller)) return false;
|
if (subAgents.Contains(caller)) return false;
|
||||||
|
|
||||||
// Technischer Fallback: nur für interne System-Operationen (Cron, ResetStale)
|
// Technischer Fallback: nur für interne System-Operationen (Cron, ResetStale)
|
||||||
@@ -129,7 +125,6 @@ public static class TaskStateHelper
|
|||||||
{
|
{
|
||||||
"backlog" => "offen",
|
"backlog" => "offen",
|
||||||
"in progress" => "inProgress",
|
"in progress" => "inProgress",
|
||||||
"delegated" => "delegated",
|
|
||||||
"review" => "review",
|
"review" => "review",
|
||||||
"blocked" => "blocked",
|
"blocked" => "blocked",
|
||||||
"done" => "done",
|
"done" => "done",
|
||||||
@@ -146,7 +141,6 @@ public static class TaskStateHelper
|
|||||||
{
|
{
|
||||||
"offen" => "Backlog",
|
"offen" => "Backlog",
|
||||||
"inprogress" => "In progress",
|
"inprogress" => "In progress",
|
||||||
"delegated" => "Delegated",
|
|
||||||
"review" => "Review",
|
"review" => "Review",
|
||||||
"blocked" => "Blocked",
|
"blocked" => "Blocked",
|
||||||
"done" => "Done",
|
"done" => "Done",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public class NexusUser
|
|||||||
/// if the underlying data is deleted. This is the single guard that
|
/// if the underlying data is deleted. This is the single guard that
|
||||||
/// prevents owner-password drift after DB resets or volume recreations.
|
/// prevents owner-password drift after DB resets or volume recreations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Table("SeedAudit")]
|
||||||
public class SeedAudit
|
public class SeedAudit
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ public sealed record AgentActivityEntry(
|
|||||||
public sealed record BoardResponse(
|
public sealed record BoardResponse(
|
||||||
List<DashboardTaskDto> Offen,
|
List<DashboardTaskDto> Offen,
|
||||||
List<DashboardTaskDto> InProgress,
|
List<DashboardTaskDto> InProgress,
|
||||||
List<DashboardTaskDto> Delegated,
|
|
||||||
List<DashboardTaskDto> Review,
|
List<DashboardTaskDto> Review,
|
||||||
List<DashboardTaskDto> Blocked,
|
List<DashboardTaskDto> Blocked,
|
||||||
List<DashboardTaskDto> Done
|
List<DashboardTaskDto> Done
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ public sealed class TaskService(
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns agent-tasks grouped by which agent is expected to respond,
|
/// Returns agent-tasks grouped by which agent is expected to respond,
|
||||||
/// with stale-detection: tasks in InProgress/Delegated that haven't been
|
/// with stale-detection: parent tasks that remain in progress while child work
|
||||||
/// updated within the stale threshold.
|
/// is active, and any in-progress task that has not been updated within the stale threshold.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<AgentWorkflowOverview> GetAgentWorkflowOverviewAsync(TimeSpan staleThreshold, CancellationToken ct = default)
|
public async Task<AgentWorkflowOverview> GetAgentWorkflowOverviewAsync(TimeSpan staleThreshold, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -194,8 +194,7 @@ public sealed class TaskService(
|
|||||||
|
|
||||||
var staleTasks = map(agentTasks
|
var staleTasks = map(agentTasks
|
||||||
.Where(t =>
|
.Where(t =>
|
||||||
(string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) &&
|
||||||
string.Equals(t.State, "Delegated", StringComparison.OrdinalIgnoreCase)) &&
|
|
||||||
t.UpdatedAt < threshold));
|
t.UpdatedAt < threshold));
|
||||||
|
|
||||||
return new AgentWorkflowOverview(waitingForBao, waitingForIris, waitingForOthers,
|
return new AgentWorkflowOverview(waitingForBao, waitingForIris, waitingForOthers,
|
||||||
@@ -214,14 +213,18 @@ public sealed class TaskService(
|
|||||||
throw new ArgumentException($"Parent task {parentTaskId} not found.", nameof(parentTaskId));
|
throw new ArgumentException($"Parent task {parentTaskId} not found.", nameof(parentTaskId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var normalizedSource = string.IsNullOrWhiteSpace(source) ? "bao" : source.Trim().ToLowerInvariant();
|
||||||
|
var normalizedAssignee = ValidateAssignedTo(assignedTo);
|
||||||
|
|
||||||
var task = new WorkTask
|
var task = new WorkTask
|
||||||
{
|
{
|
||||||
Title = title.Trim(),
|
Title = title.Trim(),
|
||||||
Detail = detail?.Trim(),
|
Detail = detail?.Trim(),
|
||||||
Source = string.IsNullOrWhiteSpace(source) ? "bao" : source.Trim(),
|
Source = normalizedSource,
|
||||||
Priority = string.IsNullOrWhiteSpace(priority) ? "Normal" : priority.Trim(),
|
Priority = string.IsNullOrWhiteSpace(priority) ? "Normal" : priority.Trim(),
|
||||||
AssignedTo = ValidateAssignedTo(assignedTo),
|
AssignedTo = normalizedAssignee,
|
||||||
ParentTaskId = parentTaskId
|
ParentTaskId = parentTaskId,
|
||||||
|
IsAgentTask = parentTaskId.HasValue
|
||||||
};
|
};
|
||||||
await taskRepo.AddAsync(task, ct);
|
await taskRepo.AddAsync(task, ct);
|
||||||
|
|
||||||
@@ -231,7 +234,7 @@ public sealed class TaskService(
|
|||||||
await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = message, TaskId = task.Id }, ct);
|
await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = message, TaskId = task.Id }, ct);
|
||||||
|
|
||||||
// Auto-notify: if assigned to bao, create a task_assigned notification
|
// Auto-notify: if assigned to bao, create a task_assigned notification
|
||||||
if (string.Equals(assignedTo, "bao", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(normalizedAssignee, "bao", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await notificationService.CreateAsync(
|
await notificationService.CreateAsync(
|
||||||
"task_assigned",
|
"task_assigned",
|
||||||
@@ -253,6 +256,7 @@ public sealed class TaskService(
|
|||||||
|
|
||||||
task.IsAgentTask = true;
|
task.IsAgentTask = true;
|
||||||
task.ExpectedFrom = string.IsNullOrWhiteSpace(expectedFrom) ? null : expectedFrom.Trim().ToLowerInvariant();
|
task.ExpectedFrom = string.IsNullOrWhiteSpace(expectedFrom) ? null : expectedFrom.Trim().ToLowerInvariant();
|
||||||
|
task.State = TaskStateHelper.ToStateString(TaskState.InProgress);
|
||||||
|
|
||||||
// Persist the agent-task-specific fields
|
// Persist the agent-task-specific fields
|
||||||
await taskRepo.UpdateAsync(task, ct);
|
await taskRepo.UpdateAsync(task, ct);
|
||||||
@@ -407,7 +411,6 @@ public sealed class TaskService(
|
|||||||
var all = await taskRepo.GetAllAsync(ct);
|
var all = await taskRepo.GetAllAsync(ct);
|
||||||
var offen = new List<DashboardTaskDto>();
|
var offen = new List<DashboardTaskDto>();
|
||||||
var inProgress = new List<DashboardTaskDto>();
|
var inProgress = new List<DashboardTaskDto>();
|
||||||
var delegated = new List<DashboardTaskDto>();
|
|
||||||
var review = new List<DashboardTaskDto>();
|
var review = new List<DashboardTaskDto>();
|
||||||
var blocked = new List<DashboardTaskDto>();
|
var blocked = new List<DashboardTaskDto>();
|
||||||
var done = new List<DashboardTaskDto>();
|
var done = new List<DashboardTaskDto>();
|
||||||
@@ -421,8 +424,6 @@ public sealed class TaskService(
|
|||||||
offen.Add(dto); break;
|
offen.Add(dto); break;
|
||||||
case "in progress":
|
case "in progress":
|
||||||
inProgress.Add(dto); break;
|
inProgress.Add(dto); break;
|
||||||
case "delegated":
|
|
||||||
delegated.Add(dto); break;
|
|
||||||
case "review":
|
case "review":
|
||||||
review.Add(dto); break;
|
review.Add(dto); break;
|
||||||
case "blocked":
|
case "blocked":
|
||||||
@@ -436,12 +437,11 @@ public sealed class TaskService(
|
|||||||
|
|
||||||
offen.Sort(SortByPriorityThenCreatedAt);
|
offen.Sort(SortByPriorityThenCreatedAt);
|
||||||
inProgress.Sort(SortByPriorityThenCreatedAt);
|
inProgress.Sort(SortByPriorityThenCreatedAt);
|
||||||
delegated.Sort(SortByPriorityThenCreatedAt);
|
|
||||||
review.Sort(SortByPriorityThenCreatedAt);
|
review.Sort(SortByPriorityThenCreatedAt);
|
||||||
blocked.Sort(SortByPriorityThenCreatedAt);
|
blocked.Sort(SortByPriorityThenCreatedAt);
|
||||||
done.Sort(SortByPriorityThenCreatedAt);
|
done.Sort(SortByPriorityThenCreatedAt);
|
||||||
|
|
||||||
return new BoardResponse(offen, inProgress, delegated, review, blocked, done);
|
return new BoardResponse(offen, inProgress, review, blocked, done);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SortByPriorityThenCreatedAt(DashboardTaskDto a, DashboardTaskDto b)
|
private static int SortByPriorityThenCreatedAt(DashboardTaskDto a, DashboardTaskDto b)
|
||||||
@@ -500,8 +500,7 @@ public sealed class TaskService(
|
|||||||
var all = await taskRepo.GetAllAsync(ct);
|
var all = await taskRepo.GetAllAsync(ct);
|
||||||
var threshold = DateTimeOffset.UtcNow - staleThreshold;
|
var threshold = DateTimeOffset.UtcNow - staleThreshold;
|
||||||
var staleTasks = all.Where(t =>
|
var staleTasks = all.Where(t =>
|
||||||
(string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) &&
|
||||||
string.Equals(t.State, "Delegated", StringComparison.OrdinalIgnoreCase)) &&
|
|
||||||
t.UpdatedAt < threshold).ToList();
|
t.UpdatedAt < threshold).ToList();
|
||||||
|
|
||||||
foreach (var task in staleTasks)
|
foreach (var task in staleTasks)
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# OpenClaw ↔ Nexus Task Board Flow
|
||||||
|
|
||||||
|
> Letzte Aktualisierung: 2026-06-21
|
||||||
|
> Status: kanonische Arbeitsbeschreibung für Iris, Sub-Agenten und das Nexus Task Board
|
||||||
|
|
||||||
|
Diese Datei beschreibt den gewünschten und umgesetzten Arbeitsfluss zwischen:
|
||||||
|
|
||||||
|
- **Bao** als Auftraggeber
|
||||||
|
- **Iris** als Chief of Staff / Koordinatorin
|
||||||
|
- **Sub-Agenten** als ausführende Spezialisten
|
||||||
|
- **OpenClaw** als Agent-Runtime
|
||||||
|
- **Nexus Task Board** als sichtbare Aufgabenquelle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Kurzfassung
|
||||||
|
|
||||||
|
**Eine Hauptaufgabe gehört Iris.**
|
||||||
|
Wenn Iris Arbeit delegiert, wird diese Delegation **nicht unsichtbar im Chat** geführt, sondern als **sichtbare Child-Task** im Nexus Task Board angelegt.
|
||||||
|
|
||||||
|
Das bedeutet:
|
||||||
|
|
||||||
|
- **Parent-Task** = Verantwortung von Iris
|
||||||
|
- **Child-Task** = konkrete Arbeitsaufgabe für einen Spezial-Agenten
|
||||||
|
- **Board** = sichtbare Wahrheit für Aufgabenstatus und Ownership
|
||||||
|
- **OpenClaw** = Ausführungspfad für Agentenarbeit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Die Hauptidee
|
||||||
|
|
||||||
|
Früher war Delegation leicht unsichtbar oder lief über einen separaten `Delegated`-Status.
|
||||||
|
|
||||||
|
Der neue Flow ersetzt das durch:
|
||||||
|
|
||||||
|
1. **Iris übernimmt eine Parent-Task**
|
||||||
|
2. **Iris zerlegt die Arbeit bei Bedarf in Subtasks**
|
||||||
|
3. **Jede echte Delegation wird als Child-Task auf dem Board angelegt**
|
||||||
|
4. **Der zuständige Agent arbeitet gegen diese Child-Task**
|
||||||
|
5. **Iris integriert die Ergebnisse zurück in die Parent-Task**
|
||||||
|
6. **Erst wenn alles fertig ist, geht die Parent-Task in Review**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Systembild
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Bao[Bao\nAuftraggeber]
|
||||||
|
Iris[Iris\nChief of Staff]
|
||||||
|
Board[Nexus Task Board\nParent + Child Tasks]
|
||||||
|
OC[OpenClaw Runtime]
|
||||||
|
Agents[Sub-Agenten\nDeveloper / Reviewer / Architekt / ...]
|
||||||
|
|
||||||
|
Bao -->|Auftrag / Priorisierung| Iris
|
||||||
|
Iris -->|legt Parent-Task an / übernimmt Task| Board
|
||||||
|
Iris -->|delegiert konkrete Arbeit| OC
|
||||||
|
OC -->|führt Agenten-Task aus| Agents
|
||||||
|
Iris -->|legt Child-Tasks an| Board
|
||||||
|
Agents -->|arbeiten gegen Child-Tasks| Board
|
||||||
|
Agents -->|liefern Ergebnis / melden Blocker| Iris
|
||||||
|
Iris -->|integriert Ergebnis| Board
|
||||||
|
Board -->|Review für Bao| Bao
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Rollen und Verantwortlichkeiten
|
||||||
|
|
||||||
|
### Bao
|
||||||
|
- gibt Aufgaben inhaltlich vor
|
||||||
|
- priorisiert und nimmt fertige Arbeit ab
|
||||||
|
- verschiebt fertige Hauptaufgaben aus **Review** nach **Done** oder zurück
|
||||||
|
|
||||||
|
### Iris
|
||||||
|
- übernimmt die Parent-Task
|
||||||
|
- analysiert, zerlegt, delegiert und reviewed
|
||||||
|
- hält die Hauptaufgabe auf dem Board aktuell
|
||||||
|
- erstellt sichtbare Child-Tasks für delegierte Arbeit
|
||||||
|
- entscheidet, ob etwas **In Progress**, **Blocked** oder **Review** ist
|
||||||
|
|
||||||
|
### Sub-Agenten
|
||||||
|
- arbeiten **nicht** direkt gegen eine diffuse Hauptaufgabe
|
||||||
|
- arbeiten gegen eine **konkret zugewiesene Child-Task**
|
||||||
|
- melden Fortschritt, Ergebnisse und Blocker an Iris
|
||||||
|
|
||||||
|
### OpenClaw
|
||||||
|
- führt die Agentenarbeit technisch aus
|
||||||
|
- liefert Nachrichten, Status und Arbeitsergebnisse zurück
|
||||||
|
- ersetzt nicht das Board als Aufgabenwahrheit
|
||||||
|
|
||||||
|
### Nexus Task Board
|
||||||
|
- ist die **sichtbare operative Quelle** für Aufgaben
|
||||||
|
- zeigt Parent-Task, Child-Tasks, Ownership und Status
|
||||||
|
- dokumentiert den tatsächlichen Arbeitsfluss
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Parent-Task vs. Child-Task
|
||||||
|
|
||||||
|
| Ebene | Zweck | Owner | Sichtbarkeit |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Parent-Task | Hauptauftrag / Koordination | Iris | Board |
|
||||||
|
| Child-Task | Delegierter Arbeitsblock | zuständiger Agent | Board |
|
||||||
|
|
||||||
|
### Parent-Task-Regeln
|
||||||
|
- bleibt bei Iris
|
||||||
|
- bleibt in der Regel **In Progress**, solange Koordination läuft
|
||||||
|
- geht erst auf **Review**, wenn alle nötigen Child-Tasks erledigt und integriert sind
|
||||||
|
- geht nur auf **Blocked**, wenn Iris insgesamt nicht weiterkommt
|
||||||
|
|
||||||
|
### Child-Task-Regeln
|
||||||
|
- repräsentiert eine echte delegierte Teilaufgabe
|
||||||
|
- hat klare Ownership (`AssignedTo`)
|
||||||
|
- zeigt sichtbar, welcher Agent woran arbeitet
|
||||||
|
- wird nicht für triviale Mini-Schritte missbraucht
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Zustandsmodell
|
||||||
|
|
||||||
|
### Parent-Task-Lifecycle
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Backlog
|
||||||
|
Backlog --> InProgress: Iris übernimmt
|
||||||
|
InProgress --> InProgress: Child-Tasks anlegen / koordinieren
|
||||||
|
InProgress --> Blocked: Gesamtblocker
|
||||||
|
InProgress --> Review: alles integriert
|
||||||
|
Review --> Done: Bao nimmt ab
|
||||||
|
Review --> Backlog: Bao gibt zurück
|
||||||
|
Blocked --> Backlog: Blocker gelöst
|
||||||
|
```
|
||||||
|
|
||||||
|
### Child-Task-Lifecycle
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Backlog
|
||||||
|
Backlog --> InProgress: Agent startet
|
||||||
|
InProgress --> Done: Ergebnis geliefert
|
||||||
|
InProgress --> Blocked: Agent kommt nicht weiter
|
||||||
|
Blocked --> Backlog: neu geplant / entsperrt
|
||||||
|
Blocked --> InProgress: Iris stößt Weiterarbeit an
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Der konkrete Arbeitsablauf
|
||||||
|
|
||||||
|
### Fall A: Bao gibt Iris einen neuen Auftrag
|
||||||
|
|
||||||
|
1. Bao formuliert einen Auftrag
|
||||||
|
2. Iris prüft Ziel, Scope, Risiko und Umgebung
|
||||||
|
3. Iris übernimmt oder erstellt die **Parent-Task**
|
||||||
|
4. Parent-Task geht auf **In Progress**
|
||||||
|
5. Wenn nötig zerlegt Iris die Arbeit in **Child-Tasks**
|
||||||
|
6. Child-Tasks werden passenden Agenten zugewiesen
|
||||||
|
7. Agenten arbeiten die Child-Tasks ab
|
||||||
|
8. Iris sammelt Ergebnisse ein und integriert sie
|
||||||
|
9. Parent-Task geht auf **Review**
|
||||||
|
10. Bao entscheidet: **Done** oder zurück nach **Backlog**
|
||||||
|
|
||||||
|
### Fall B: Agent meldet einen Blocker
|
||||||
|
|
||||||
|
1. Agent meldet Blocker an Iris
|
||||||
|
2. Iris prüft, ob der Blocker lokal lösbar ist
|
||||||
|
3. Wenn nein: die betroffene **Child-Task** geht auf **Blocked**
|
||||||
|
4. Falls nötig entsteht eine neue Ursachen-Task / neue Child-Task
|
||||||
|
5. Parent-Task bleibt **In Progress**, solange der Gesamtauftrag noch koordiniert wird
|
||||||
|
6. Nur wenn die Hauptaufgabe insgesamt feststeckt, geht die **Parent-Task** auf **Blocked**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. OpenClaw- und Board-Interaktion
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Bao
|
||||||
|
participant Iris
|
||||||
|
participant Board as Nexus Task Board
|
||||||
|
participant OpenClaw
|
||||||
|
participant Agent as Sub-Agent
|
||||||
|
|
||||||
|
Bao->>Iris: Auftrag
|
||||||
|
Iris->>Board: Parent-Task übernehmen / anlegen
|
||||||
|
Iris->>Board: Child-Task anlegen
|
||||||
|
Iris->>OpenClaw: Agentenauftrag starten
|
||||||
|
OpenClaw->>Agent: Task ausführen
|
||||||
|
Agent-->>Iris: Ergebnis / Rückfrage / Blocker
|
||||||
|
Iris->>Board: Child-Task aktualisieren
|
||||||
|
Iris->>Board: Parent-Task integrieren
|
||||||
|
Iris->>Board: Parent auf Review setzen
|
||||||
|
Board-->>Bao: Review sichtbar
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Regeln für gutes Schneiden von Child-Tasks
|
||||||
|
|
||||||
|
Eine Child-Task ist sinnvoll, wenn sie:
|
||||||
|
|
||||||
|
- einen **klaren Arbeitsblock** darstellt
|
||||||
|
- einen **eigenen Verantwortlichen** hat
|
||||||
|
- ein **eigenes Ergebnis** liefern soll
|
||||||
|
- unabhängig als **Done** oder **Blocked** sichtbar sein kann
|
||||||
|
|
||||||
|
Keine gute Child-Task ist:
|
||||||
|
|
||||||
|
- „Datei öffnen"
|
||||||
|
- „kurz nachschauen"
|
||||||
|
- „eine Kleinigkeit prüfen"
|
||||||
|
|
||||||
|
Faustregel:
|
||||||
|
|
||||||
|
> **Eine Child-Task soll ein echter delegierbarer Arbeitsauftrag sein, kein Mikro-Schritt.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Board-Sicht: was sichtbar sein soll
|
||||||
|
|
||||||
|
Im Board soll erkennbar sein:
|
||||||
|
|
||||||
|
- welche Parent-Task Iris gerade steuert
|
||||||
|
- welche Child-Tasks darunter existieren
|
||||||
|
- welcher Agent welche Child-Task besitzt
|
||||||
|
- welche Child-Task blockiert ist
|
||||||
|
- welche Parent-Task in Review auf Bao wartet
|
||||||
|
|
||||||
|
Im Task-Detail sollen sichtbar sein:
|
||||||
|
|
||||||
|
- Parent/Child-Beziehung
|
||||||
|
- `AssignedTo`
|
||||||
|
- Status
|
||||||
|
- erwarteter nächster Beitrag / letzter Aktivitätshinweis
|
||||||
|
- Child-Task-Liste direkt unter der Parent-Task
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Kanonische Regeln
|
||||||
|
|
||||||
|
### Regel 1 — Das Board ist die sichtbare Aufgabenwahrheit
|
||||||
|
Chat und Agentenläufe ergänzen das Board, ersetzen es aber nicht.
|
||||||
|
|
||||||
|
### Regel 2 — Iris bleibt Ownerin der Hauptaufgabe
|
||||||
|
Delegation verschiebt Verantwortung nicht automatisch auf den Agenten.
|
||||||
|
|
||||||
|
### Regel 3 — Delegation ist sichtbar
|
||||||
|
Jede echte delegierte Arbeit wird als Child-Task abgebildet.
|
||||||
|
|
||||||
|
### Regel 4 — Kein künstlicher Wartezustand auf Parent-Ebene
|
||||||
|
Die Parent-Task bleibt **In Progress**, solange Iris aktiv koordiniert.
|
||||||
|
|
||||||
|
### Regel 5 — Blocker präzise markieren
|
||||||
|
Wenn nur ein Arbeitspaket hängt, blockiert zuerst die **Child-Task**, nicht automatisch die ganze Parent-Task.
|
||||||
|
|
||||||
|
### Regel 6 — Review ist Bao-Gate
|
||||||
|
Fertige Hauptaufgaben gehen erst in **Review**, dann nach Bao-Entscheid auf **Done** oder zurück.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Beispiel
|
||||||
|
|
||||||
|
### Parent-Task
|
||||||
|
**„Nexus Taskflow auf Parent-/Child-Modell umstellen“** — Owner: `iris`
|
||||||
|
|
||||||
|
### Mögliche Child-Tasks
|
||||||
|
- **Backend-State-Handling anpassen** — Owner: `developer`
|
||||||
|
- **Frontend-Board-Spalten und Labels anpassen** — Owner: `developer`
|
||||||
|
- **Workflow verifizieren / Regression prüfen** — Owner: `reviewer`
|
||||||
|
- **Deploy-/Runtime-Auswirkung prüfen** — Owner: `architekt`
|
||||||
|
|
||||||
|
So sieht Bao später nicht nur „Iris arbeitet daran“, sondern konkret:
|
||||||
|
|
||||||
|
- welcher Teil erledigt ist
|
||||||
|
- welcher Teil noch läuft
|
||||||
|
- welcher Teil blockiert ist
|
||||||
|
- worauf Iris gerade wartet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Anti-Patterns
|
||||||
|
|
||||||
|
Diese Muster sollen vermieden werden:
|
||||||
|
|
||||||
|
- Parent-Task auf einen bloßen **Delegated**-Wartestatus schieben
|
||||||
|
- Delegation nur im Chat sichtbar machen
|
||||||
|
- Child-Tasks ohne klare Ownership anlegen
|
||||||
|
- Blocker nur mündlich erwähnen, aber nicht im Board markieren
|
||||||
|
- zehn Mikro-Subtasks für einen Mini-Arbeitsschritt erzeugen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Entscheidungsregel für Iris
|
||||||
|
|
||||||
|
Wenn Iris unsicher ist, ob sie eine Child-Task anlegen soll, gilt:
|
||||||
|
|
||||||
|
**Child-Task anlegen**, wenn mindestens einer der Punkte zutrifft:
|
||||||
|
|
||||||
|
- anderer Agent übernimmt echte Arbeit
|
||||||
|
- eigener Status muss sichtbar verfolgt werden
|
||||||
|
- eigener Blocker ist möglich
|
||||||
|
- Bao soll Transparenz über diesen Teil sehen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Technische Leitplanken
|
||||||
|
|
||||||
|
- `parentTaskId` verknüpft Child-Tasks mit der Parent-Task
|
||||||
|
- `AssignedTo` zeigt den operativen Owner
|
||||||
|
- Agentenstatus und Boardstatus dürfen sich ergänzen, aber nicht widersprechen
|
||||||
|
- Board-Spalten und API-State-Mapping müssen das Parent-/Child-Modell sauber abbilden
|
||||||
|
- UI und Doku müssen dieselbe Sprache sprechen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Merksatz
|
||||||
|
|
||||||
|
> **Iris koordiniert die Hauptaufgabe. Agenten erledigen sichtbare Child-Tasks. Das Board zeigt die Wahrheit. OpenClaw führt aus.**
|
||||||
@@ -7,6 +7,8 @@ export async function apiFetch(input: RequestInfo | URL, init: RequestInit = {})
|
|||||||
const send = () => {
|
const send = () => {
|
||||||
const headers = new Headers(init.headers)
|
const headers = new Headers(init.headers)
|
||||||
if (auth.accessToken) headers.set('Authorization', `Bearer ${auth.accessToken}`)
|
if (auth.accessToken) headers.set('Authorization', `Bearer ${auth.accessToken}`)
|
||||||
|
if (auth.isIris) headers.set('X-Agent-Id', 'iris')
|
||||||
|
else if (auth.isBao) headers.set('X-Agent-Id', 'bao')
|
||||||
return fetch(input, { ...init, headers, credentials: 'include' })
|
return fetch(input, { ...init, headers, credentials: 'include' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Fetches tasks from /api/dashboard/tasks and /api/dashboard/tasks/board
|
* Fetches tasks from /api/dashboard/tasks and /api/dashboard/tasks/board
|
||||||
* and maps them into TaskItem[] format for the TaskStrip component.
|
* and maps them into TaskItem[] format for the TaskStrip component.
|
||||||
*
|
*
|
||||||
* Board state: grouped by column (offen, inProgress, delegated, review, done, blocked)
|
* Board state: grouped by column (offen, inProgress, review, blocked, done)
|
||||||
* Auto-refresh: every 30 seconds.
|
* Auto-refresh: every 30 seconds.
|
||||||
*/
|
*/
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
@@ -34,10 +34,9 @@ export interface DashboardTaskDto {
|
|||||||
export interface BoardGroup {
|
export interface BoardGroup {
|
||||||
offen: DashboardTaskDto[]
|
offen: DashboardTaskDto[]
|
||||||
inProgress: DashboardTaskDto[]
|
inProgress: DashboardTaskDto[]
|
||||||
delegated: DashboardTaskDto[]
|
|
||||||
review: DashboardTaskDto[]
|
review: DashboardTaskDto[]
|
||||||
done: DashboardTaskDto[]
|
|
||||||
blocked: DashboardTaskDto[]
|
blocked: DashboardTaskDto[]
|
||||||
|
done: DashboardTaskDto[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentWorkflowOverview {
|
export interface AgentWorkflowOverview {
|
||||||
@@ -97,10 +96,9 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
board: {
|
board: {
|
||||||
offen: [] as DashboardTaskDto[],
|
offen: [] as DashboardTaskDto[],
|
||||||
inProgress: [] as DashboardTaskDto[],
|
inProgress: [] as DashboardTaskDto[],
|
||||||
delegated: [] as DashboardTaskDto[],
|
|
||||||
review: [] as DashboardTaskDto[],
|
review: [] as DashboardTaskDto[],
|
||||||
done: [] as DashboardTaskDto[],
|
|
||||||
blocked: [] as DashboardTaskDto[],
|
blocked: [] as DashboardTaskDto[],
|
||||||
|
done: [] as DashboardTaskDto[],
|
||||||
} as BoardGroup,
|
} as BoardGroup,
|
||||||
boardLoading: false,
|
boardLoading: false,
|
||||||
boardError: null as string | null,
|
boardError: null as string | null,
|
||||||
@@ -169,10 +167,9 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
const canonicalMap: Record<string, string> = {
|
const canonicalMap: Record<string, string> = {
|
||||||
offen: 'Backlog',
|
offen: 'Backlog',
|
||||||
inProgress: 'In progress',
|
inProgress: 'In progress',
|
||||||
delegated: 'Delegated',
|
|
||||||
review: 'Review',
|
review: 'Review',
|
||||||
done: 'Done',
|
|
||||||
blocked: 'Blocked',
|
blocked: 'Blocked',
|
||||||
|
done: 'Done',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save previous state for rollback
|
// Save previous state for rollback
|
||||||
@@ -188,7 +185,6 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
const task =
|
const task =
|
||||||
findAndRemove(this.board.offen) ??
|
findAndRemove(this.board.offen) ??
|
||||||
findAndRemove(this.board.inProgress) ??
|
findAndRemove(this.board.inProgress) ??
|
||||||
findAndRemove(this.board.delegated) ??
|
|
||||||
findAndRemove(this.board.review) ??
|
findAndRemove(this.board.review) ??
|
||||||
findAndRemove(this.board.blocked) ??
|
findAndRemove(this.board.blocked) ??
|
||||||
findAndRemove(this.board.done)
|
findAndRemove(this.board.done)
|
||||||
@@ -322,6 +318,7 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
priority?: string
|
priority?: string
|
||||||
assignedTo?: string
|
assignedTo?: string
|
||||||
expectedFrom?: string
|
expectedFrom?: string
|
||||||
|
parentTaskId?: string | null
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch('/api/dashboard/tasks/agent', {
|
const res = await apiFetch('/api/dashboard/tasks/agent', {
|
||||||
@@ -333,6 +330,7 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
priority: data.priority ?? 'Medium',
|
priority: data.priority ?? 'Medium',
|
||||||
assignedTo: data.assignedTo ?? null,
|
assignedTo: data.assignedTo ?? null,
|
||||||
expectedFrom: data.expectedFrom ?? null,
|
expectedFrom: data.expectedFrom ?? null,
|
||||||
|
parentTaskId: data.parentTaskId ?? null,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export interface RoutingTarget {
|
|||||||
detail: string
|
detail: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskState = 'Backlog' | 'In progress' | 'Blocked' | 'Done'
|
export type TaskState = 'Backlog' | 'In progress' | 'Review' | 'Blocked' | 'Done'
|
||||||
export const TASK_STATES: TaskState[] = ['Backlog', 'In progress', 'Blocked', 'Done']
|
export const TASK_STATES: TaskState[] = ['Backlog', 'In progress', 'Review', 'Blocked', 'Done']
|
||||||
|
|
||||||
export interface OperationsSnapshot {
|
export interface OperationsSnapshot {
|
||||||
generatedAt: string
|
generatedAt: string
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
* TaskBoardView – Linear-style Kanban Board
|
* TaskBoardView – Linear-style Kanban Board
|
||||||
* Galaxy/Dashboard V2 styled edition.
|
* Galaxy/Dashboard V2 styled edition.
|
||||||
*
|
*
|
||||||
* 6 columns: Offen, In Bearbeitung, Delegiert, Review, Blockiert, Erledigt
|
* 5 columns: Offen, In Bearbeitung, Review, Blockiert, Erledigt
|
||||||
* HTML5 Drag & Drop (no external lib)
|
* HTML5 Drag & Drop (no external lib)
|
||||||
*
|
*
|
||||||
* Agent-Workflow Features:
|
* Agent-Workflow Features:
|
||||||
* - Agent-Tasks have a 🤖 badge
|
* - Agent-Tasks have a 🤖 badge
|
||||||
* - ExpectedFrom field shows who is expected to act next
|
* - ExpectedFrom field shows who is expected to act next
|
||||||
* - Stale-task warning banner at top (InProgress/Delegated > 2h)
|
* - Stale-task warning banner at top (InProgress > 2h)
|
||||||
* - Waiting section for Iris overview
|
* - Waiting section for Iris overview
|
||||||
*/
|
*/
|
||||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
@@ -157,7 +157,6 @@ function statusTone(state: string): string {
|
|||||||
case 'done': return 'is-done'
|
case 'done': return 'is-done'
|
||||||
case 'blocked': return 'is-blocked'
|
case 'blocked': return 'is-blocked'
|
||||||
case 'review': return 'is-review'
|
case 'review': return 'is-review'
|
||||||
case 'delegated': return 'is-delegated'
|
|
||||||
case 'in progress': return 'is-progress'
|
case 'in progress': return 'is-progress'
|
||||||
default: return 'is-backlog'
|
default: return 'is-backlog'
|
||||||
}
|
}
|
||||||
@@ -183,7 +182,6 @@ function flattenBoard() {
|
|||||||
return [
|
return [
|
||||||
...taskStore.board.offen,
|
...taskStore.board.offen,
|
||||||
...taskStore.board.inProgress,
|
...taskStore.board.inProgress,
|
||||||
...taskStore.board.delegated,
|
|
||||||
...taskStore.board.review,
|
...taskStore.board.review,
|
||||||
...taskStore.board.blocked,
|
...taskStore.board.blocked,
|
||||||
...taskStore.board.done,
|
...taskStore.board.done,
|
||||||
@@ -250,8 +248,45 @@ function relativeTime(date?: string | null): string {
|
|||||||
return `vor ${days} d`
|
return `vor ${days} d`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function childStatusSummary(taskId: string): string {
|
||||||
|
const children = allBoardTasks.value.filter(task => task.parentTaskId === taskId)
|
||||||
|
if (!children.length) return ''
|
||||||
|
|
||||||
|
const counts = {
|
||||||
|
inProgress: children.filter(task => task.state === 'In progress').length,
|
||||||
|
review: children.filter(task => task.state === 'Review').length,
|
||||||
|
blocked: children.filter(task => task.state === 'Blocked').length,
|
||||||
|
done: children.filter(task => task.state === 'Done').length,
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [] as string[]
|
||||||
|
if (counts.inProgress) parts.push(`${counts.inProgress} in Arbeit`)
|
||||||
|
if (counts.review) parts.push(`${counts.review} im Review`)
|
||||||
|
if (counts.blocked) parts.push(`${counts.blocked} blockiert`)
|
||||||
|
if (counts.done) parts.push(`${counts.done} erledigt`)
|
||||||
|
return parts.length ? `Child-Tasks: ${parts.join(' · ')}` : `Child-Tasks: ${children.length}`
|
||||||
|
}
|
||||||
|
|
||||||
function activityHint(task: BoardTask): string {
|
function activityHint(task: BoardTask): string {
|
||||||
return task.lastActivityMessage?.trim() || (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status')
|
return task.lastActivityMessage?.trim()
|
||||||
|
|| childStatusSummary(task.id)
|
||||||
|
|| (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasChildTasks(taskId: string): boolean {
|
||||||
|
return allBoardTasks.value.some(task => task.parentTaskId === taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assigneeLabel(assignedTo: string | null | undefined): string {
|
||||||
|
return expectedFromLabel(assignedTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assigneeClass(assignedTo: string | null | undefined): string {
|
||||||
|
if (!assignedTo) return ''
|
||||||
|
const lower = assignedTo.toLowerCase()
|
||||||
|
if (lower === 'iris') return 'assignee-iris'
|
||||||
|
if (lower === 'bao') return 'assignee-bao'
|
||||||
|
return 'assignee-agent'
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Task Navigation ───────────────────────────── */
|
/* ── Task Navigation ───────────────────────────── */
|
||||||
@@ -399,7 +434,7 @@ onUnmounted(() => {
|
|||||||
<!-- Stale Warning Banner -->
|
<!-- Stale Warning Banner -->
|
||||||
<div v-if="staleCount > 0" class="stale-banner">
|
<div v-if="staleCount > 0" class="stale-banner">
|
||||||
<AlertTriangle :size="14" />
|
<AlertTriangle :size="14" />
|
||||||
<span><strong>{{ staleCount }} Task(s)</strong> sind stale (InBearbeitung/Delegiert > 2h ohne Update).</span>
|
<span><strong>{{ staleCount }} Task(s)</strong> sind stale (In Bearbeitung > 2h ohne Update).</span>
|
||||||
<button class="stale-dismiss" @click="showIrisPanel = true">Ansehen</button>
|
<button class="stale-dismiss" @click="showIrisPanel = true">Ansehen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -514,14 +549,14 @@ onUnmounted(() => {
|
|||||||
<span
|
<span
|
||||||
v-if="task.assignedTo"
|
v-if="task.assignedTo"
|
||||||
class="assignee"
|
class="assignee"
|
||||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
:class="assigneeClass(task.assignedTo)"
|
||||||
>
|
>
|
||||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
{{ assigneeLabel(task.assignedTo) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-title">{{ task.title }}</div>
|
<div class="card-title">{{ task.title }}</div>
|
||||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!taskStore.board.offen.length" class="empty-col">Keine Aufgaben</div>
|
<div v-if="!taskStore.board.offen.length" class="empty-col">Keine Aufgaben</div>
|
||||||
@@ -563,69 +598,20 @@ onUnmounted(() => {
|
|||||||
<span
|
<span
|
||||||
v-if="task.assignedTo"
|
v-if="task.assignedTo"
|
||||||
class="assignee"
|
class="assignee"
|
||||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
:class="assigneeClass(task.assignedTo)"
|
||||||
>
|
>
|
||||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
{{ assigneeLabel(task.assignedTo) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-title">{{ task.title }}</div>
|
<div class="card-title">{{ task.title }}</div>
|
||||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!taskStore.board.inProgress.length" class="empty-col">Keine Aufgaben</div>
|
<div v-if="!taskStore.board.inProgress.length" class="empty-col">Keine Aufgaben</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
class="col"
|
|
||||||
:class="{ 'drag-over': dragOverColumn === 'delegated' }"
|
|
||||||
@dragover="onDragOver($event, 'delegated')"
|
|
||||||
@dragleave="onDragLeave"
|
|
||||||
@drop="onDrop($event, 'delegated')"
|
|
||||||
>
|
|
||||||
<div class="col-header">
|
|
||||||
<span class="col-icon" style="background: var(--a-purple); box-shadow: 0 0 0 2px rgba(181,87,246,.25);"></span>
|
|
||||||
<span class="col-name">Delegiert</span>
|
|
||||||
<span class="col-count">{{ taskStore.board.delegated.length }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-cards">
|
|
||||||
<button
|
|
||||||
v-for="task in taskStore.board.delegated"
|
|
||||||
:key="task.id"
|
|
||||||
type="button"
|
|
||||||
class="card"
|
|
||||||
:class="{ 'card-agent': task.isAgentTask }"
|
|
||||||
draggable="true"
|
|
||||||
@click="handleCardClick($event, task.id)"
|
|
||||||
@dragstart="onDragStart($event, task.id)"
|
|
||||||
@dragend="onDragEnd"
|
|
||||||
>
|
|
||||||
<div class="card-top">
|
|
||||||
<span class="prio-badge" :style="{ color: priorityColor(task.priority), borderColor: priorityColor(task.priority) }">
|
|
||||||
{{ priorityLabel(task.priority) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="task.isAgentTask" class="agent-badge" title="Agent-Task">🤖</span>
|
|
||||||
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
|
|
||||||
⏳ {{ task.expectedFrom }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="task.assignedTo"
|
|
||||||
class="assignee"
|
|
||||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
|
||||||
>
|
|
||||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-title">{{ task.title }}</div>
|
|
||||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
|
||||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
|
||||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
|
||||||
</button>
|
|
||||||
<div v-if="!taskStore.board.delegated.length" class="empty-col">Keine delegierten Aufgaben</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col"
|
class="col"
|
||||||
:class="{ 'drag-over': dragOverColumn === 'review' }"
|
:class="{ 'drag-over': dragOverColumn === 'review' }"
|
||||||
@@ -661,14 +647,14 @@ onUnmounted(() => {
|
|||||||
<span
|
<span
|
||||||
v-if="task.assignedTo"
|
v-if="task.assignedTo"
|
||||||
class="assignee"
|
class="assignee"
|
||||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
:class="assigneeClass(task.assignedTo)"
|
||||||
>
|
>
|
||||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
{{ assigneeLabel(task.assignedTo) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-title">{{ task.title }}</div>
|
<div class="card-title">{{ task.title }}</div>
|
||||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!taskStore.board.review.length" class="empty-col">Keine Aufgaben</div>
|
<div v-if="!taskStore.board.review.length" class="empty-col">Keine Aufgaben</div>
|
||||||
@@ -705,14 +691,14 @@ onUnmounted(() => {
|
|||||||
<span
|
<span
|
||||||
v-if="task.assignedTo"
|
v-if="task.assignedTo"
|
||||||
class="assignee"
|
class="assignee"
|
||||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
:class="assigneeClass(task.assignedTo)"
|
||||||
>
|
>
|
||||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
{{ assigneeLabel(task.assignedTo) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-title">{{ task.title }}</div>
|
<div class="card-title">{{ task.title }}</div>
|
||||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!taskStore.board.done.length" class="empty-col">Keine Aufgaben</div>
|
<div v-if="!taskStore.board.done.length" class="empty-col">Keine Aufgaben</div>
|
||||||
@@ -754,13 +740,14 @@ onUnmounted(() => {
|
|||||||
<span
|
<span
|
||||||
v-if="task.assignedTo"
|
v-if="task.assignedTo"
|
||||||
class="assignee"
|
class="assignee"
|
||||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
:class="assigneeClass(task.assignedTo)"
|
||||||
>
|
>
|
||||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
{{ assigneeLabel(task.assignedTo) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-title">{{ task.title }}</div>
|
<div class="card-title">{{ task.title }}</div>
|
||||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||||
|
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!taskStore.board.blocked.length" class="empty-col">Keine Blockierer</div>
|
<div v-if="!taskStore.board.blocked.length" class="empty-col">Keine Blockierer</div>
|
||||||
@@ -849,11 +836,12 @@ onUnmounted(() => {
|
|||||||
<span>#{{ selectedTask.id.slice(0, 8) }}</span>
|
<span>#{{ selectedTask.id.slice(0, 8) }}</span>
|
||||||
<span v-if="selectedTask.isAgentTask" class="meta-agent-tag">🤖 Agent-Task</span>
|
<span v-if="selectedTask.isAgentTask" class="meta-agent-tag">🤖 Agent-Task</span>
|
||||||
<span v-if="selectedTask.expectedFrom" class="meta-expected">⏳ Erwartet: {{ selectedTask.expectedFrom }}</span>
|
<span v-if="selectedTask.expectedFrom" class="meta-expected">⏳ Erwartet: {{ selectedTask.expectedFrom }}</span>
|
||||||
|
<span v-if="selectedTask.parentTaskId" class="meta-expected">↳ Child-Task</span>
|
||||||
<span><Clock3 :size="13" /> Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }}</span>
|
<span><Clock3 :size="13" /> Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }}</span>
|
||||||
<span><CalendarDays :size="13" /> Erstellt {{ formatDate(selectedTask.createdAt) }}</span>
|
<span><CalendarDays :size="13" /> Erstellt {{ formatDate(selectedTask.createdAt) }}</span>
|
||||||
<span v-if="selectedTask.isAgentTask"><MessageSquareText :size="13" /> Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }}</span>
|
<span v-if="selectedTask.isAgentTask"><MessageSquareText :size="13" /> Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedTask.isAgentTask" class="detail-progress-banner">
|
<div v-if="selectedTask.isAgentTask || hasChildTasks(selectedTask.id)" class="detail-progress-banner">
|
||||||
<strong>Letzter Fortschritt:</strong> {{ activityHint(selectedTask) }}
|
<strong>Letzter Fortschritt:</strong> {{ activityHint(selectedTask) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -914,7 +902,6 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<option value="Backlog">Offen</option>
|
<option value="Backlog">Offen</option>
|
||||||
<option value="In progress">In Bearbeitung</option>
|
<option value="In progress">In Bearbeitung</option>
|
||||||
<option value="Delegated">Delegiert</option>
|
|
||||||
<option value="Review">Review</option>
|
<option value="Review">Review</option>
|
||||||
<option value="Blocked">Blockiert</option>
|
<option value="Blocked">Blockiert</option>
|
||||||
<option value="Done">Erledigt</option>
|
<option value="Done">Erledigt</option>
|
||||||
@@ -970,6 +957,10 @@ onUnmounted(() => {
|
|||||||
<dt>Erwartet von</dt>
|
<dt>Erwartet von</dt>
|
||||||
<dd>{{ selectedTask.expectedFrom }}</dd>
|
<dd>{{ selectedTask.expectedFrom }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedTask.parentTaskId">
|
||||||
|
<dt>Task-Typ</dt>
|
||||||
|
<dd>Sichtbare Child-Task</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -1078,6 +1069,7 @@ select:disabled { opacity: .45; cursor: not-allowed; }
|
|||||||
.assignee { font-family: 'Manrope', sans-serif; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
|
.assignee { font-family: 'Manrope', sans-serif; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
|
||||||
.assignee-iris { background: rgba(147, 51, 234, .12); color: #c084fc; }
|
.assignee-iris { background: rgba(147, 51, 234, .12); color: #c084fc; }
|
||||||
.assignee-bao { background: rgba(59, 130, 246, .12); color: #60a5fa; }
|
.assignee-bao { background: rgba(59, 130, 246, .12); color: #60a5fa; }
|
||||||
|
.assignee-agent { background: rgba(16, 185, 129, .12); color: #6ee7b7; }
|
||||||
.card-title { font-size: 12.5px; font-weight: 600; color: var(--tx); line-height: 1.4; word-break: break-word; font-family: 'Manrope', sans-serif; }
|
.card-title { font-size: 12.5px; font-weight: 600; color: var(--tx); line-height: 1.4; word-break: break-word; font-family: 'Manrope', sans-serif; }
|
||||||
.card-preview { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--tx-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
.card-preview { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--tx-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
.card-progress-hint { margin-top: 7px; font-size: 10.5px; color: var(--tx-2); line-height: 1.4; padding: 6px 8px; border-radius: 8px; background: rgba(124,108,255,.07); border: 1px solid rgba(124,108,255,.12); }
|
.card-progress-hint { margin-top: 7px; font-size: 10.5px; color: var(--tx-2); line-height: 1.4; padding: 6px 8px; border-radius: 8px; background: rgba(124,108,255,.07); border: 1px solid rgba(124,108,255,.12); }
|
||||||
@@ -1123,7 +1115,6 @@ select:disabled { opacity: .45; cursor: not-allowed; }
|
|||||||
.detail-state-pill { width: fit-content; border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; }
|
.detail-state-pill { width: fit-content; border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; }
|
||||||
.detail-state-pill.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); }
|
.detail-state-pill.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); }
|
||||||
.detail-state-pill.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
.detail-state-pill.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||||
.detail-state-pill.is-delegated { color: #d8b4fe; background: rgba(168,85,247,.12); border-color: rgba(168,85,247,.25); }
|
|
||||||
.detail-state-pill.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); }
|
.detail-state-pill.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); }
|
||||||
.detail-state-pill.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); }
|
.detail-state-pill.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); }
|
||||||
.detail-state-pill.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
.detail-state-pill.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ function statusLabel(state: string): string {
|
|||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'Backlog': 'Offen',
|
'Backlog': 'Offen',
|
||||||
'In progress': 'In Bearbeitung',
|
'In progress': 'In Bearbeitung',
|
||||||
'Delegated': 'Delegiert',
|
|
||||||
'Review': 'Review',
|
'Review': 'Review',
|
||||||
'Blocked': 'Blockiert',
|
'Blocked': 'Blockiert',
|
||||||
'Done': 'Erledigt',
|
'Done': 'Erledigt',
|
||||||
@@ -105,7 +104,6 @@ function statusClass(state: string): string {
|
|||||||
if (s === 'done') return 'is-done'
|
if (s === 'done') return 'is-done'
|
||||||
if (s === 'blocked') return 'is-blocked'
|
if (s === 'blocked') return 'is-blocked'
|
||||||
if (s === 'review') return 'is-review'
|
if (s === 'review') return 'is-review'
|
||||||
if (s === 'delegated') return 'is-delegated'
|
|
||||||
if (s === 'in progress') return 'is-progress'
|
if (s === 'in progress') return 'is-progress'
|
||||||
return 'is-backlog'
|
return 'is-backlog'
|
||||||
}
|
}
|
||||||
@@ -148,8 +146,27 @@ function relativeTime(date?: string | null): string {
|
|||||||
return `vor ${days} d`
|
return `vor ${days} d`
|
||||||
}
|
}
|
||||||
|
|
||||||
function progressHint(taskLike: Pick<TaskDto, 'lastActivityMessage' | 'expectedFrom'>): string {
|
function childStatusSummary(taskId: string): string {
|
||||||
return taskLike.lastActivityMessage?.trim() || (taskLike.expectedFrom ? `Wartet auf ${taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status')
|
const childItems = children.value.filter(child => child.parentTaskId === taskId)
|
||||||
|
if (!childItems.length) return ''
|
||||||
|
|
||||||
|
const counts = {
|
||||||
|
inProgress: childItems.filter(child => child.state === 'In progress').length,
|
||||||
|
review: childItems.filter(child => child.state === 'Review').length,
|
||||||
|
blocked: childItems.filter(child => child.state === 'Blocked').length,
|
||||||
|
done: childItems.filter(child => child.state === 'Done').length,
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [] as string[]
|
||||||
|
if (counts.inProgress) parts.push(`${counts.inProgress} in Arbeit`)
|
||||||
|
if (counts.review) parts.push(`${counts.review} im Review`)
|
||||||
|
if (counts.blocked) parts.push(`${counts.blocked} blockiert`)
|
||||||
|
if (counts.done) parts.push(`${counts.done} erledigt`)
|
||||||
|
return parts.length ? `Child-Tasks: ${parts.join(' · ')}` : `Child-Tasks: ${childItems.length}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressHint(taskLike: Pick<TaskDto, 'id' | 'lastActivityMessage' | 'expectedFrom'>): string {
|
||||||
|
return taskLike.lastActivityMessage?.trim() || childStatusSummary(taskLike.id) || (taskLike.expectedFrom ? `Wartet auf ${taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status')
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── API calls ───────────────────────────────── */
|
/* ── API calls ───────────────────────────────── */
|
||||||
@@ -362,6 +379,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<span class="meta-chip">{{ task.priority }} Priorität</span>
|
<span class="meta-chip">{{ task.priority }} Priorität</span>
|
||||||
<span v-if="task.isAgentTask" class="meta-chip">🤖 Agent-Task</span>
|
<span v-if="task.isAgentTask" class="meta-chip">🤖 Agent-Task</span>
|
||||||
<span v-if="task.expectedFrom" class="meta-chip">⏳ Erwartet: {{ task.expectedFrom }}</span>
|
<span v-if="task.expectedFrom" class="meta-chip">⏳ Erwartet: {{ task.expectedFrom }}</span>
|
||||||
|
<span v-if="task.parentTaskId" class="meta-chip">↳ Sichtbare Child-Task</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -390,7 +408,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="task.isAgentTask" class="progress-banner">
|
<div v-if="task.isAgentTask || childStatusSummary(task.id)" class="progress-banner">
|
||||||
<strong>Letzter Fortschritt:</strong> {{ progressHint(task) }}
|
<strong>Letzter Fortschritt:</strong> {{ progressHint(task) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -424,7 +442,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<option value="Medium">Medium</option>
|
<option value="Medium">Medium</option>
|
||||||
<option value="Low">Low</option>
|
<option value="Low">Low</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="subtaskAssign" class="galaxy-input narrow" placeholder="Zuständig (bao, iris…)" />
|
<input v-model="subtaskAssign" class="galaxy-input narrow" placeholder="Zuständig (bao, iris, researcher…)" />
|
||||||
<button class="btn-primary btn-sm" @click="createSubtask" :disabled="creatingSubtask">
|
<button class="btn-primary btn-sm" @click="createSubtask" :disabled="creatingSubtask">
|
||||||
{{ creatingSubtask ? 'Erstelle…' : 'Anlegen' }}
|
{{ creatingSubtask ? 'Erstelle…' : 'Anlegen' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -455,6 +473,8 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
>
|
>
|
||||||
<option value="Backlog">Offen</option>
|
<option value="Backlog">Offen</option>
|
||||||
<option value="In progress">In Arbeit</option>
|
<option value="In progress">In Arbeit</option>
|
||||||
|
<option value="Review">Review</option>
|
||||||
|
<option value="Blocked">Blockiert</option>
|
||||||
<option value="Done">Erledigt</option>
|
<option value="Done">Erledigt</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
@@ -520,7 +540,6 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<select v-model="form.state" class="galaxy-input galaxy-select" :disabled="!canChangeState">
|
<select v-model="form.state" class="galaxy-input galaxy-select" :disabled="!canChangeState">
|
||||||
<option value="Backlog">Offen</option>
|
<option value="Backlog">Offen</option>
|
||||||
<option value="In progress">In Bearbeitung</option>
|
<option value="In progress">In Bearbeitung</option>
|
||||||
<option value="Delegated">Delegiert</option>
|
|
||||||
<option value="Review">Review</option>
|
<option value="Review">Review</option>
|
||||||
<option value="Blocked">Blockiert</option>
|
<option value="Blocked">Blockiert</option>
|
||||||
<option value="Done">Erledigt</option>
|
<option value="Done">Erledigt</option>
|
||||||
@@ -543,6 +562,8 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<option value="programmer">🛠 Programmer</option>
|
<option value="programmer">🛠 Programmer</option>
|
||||||
<option value="reviewer">🔎 Reviewer</option>
|
<option value="reviewer">🔎 Reviewer</option>
|
||||||
<option value="architekt">🏛 Architekt</option>
|
<option value="architekt">🏛 Architekt</option>
|
||||||
|
<option value="researcher">🔬 Researcher</option>
|
||||||
|
<option value="executor">⚡ Executor</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="sidebar-field">
|
<label class="sidebar-field">
|
||||||
@@ -559,6 +580,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<div><dt>Erstellt</dt><dd>{{ formatDate(task.createdAt) }}</dd></div>
|
<div><dt>Erstellt</dt><dd>{{ formatDate(task.createdAt) }}</dd></div>
|
||||||
<div><dt>Geändert</dt><dd>{{ formatDate(task.updatedAt, true) }}</dd></div>
|
<div><dt>Geändert</dt><dd>{{ formatDate(task.updatedAt, true) }}</dd></div>
|
||||||
<div v-if="task.isAgentTask"><dt>Letzter Status</dt><dd>{{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</dd></div>
|
<div v-if="task.isAgentTask"><dt>Letzter Status</dt><dd>{{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</dd></div>
|
||||||
|
<div v-if="task.parentTaskId"><dt>Task-Typ</dt><dd>Sichtbare Child-Task</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -678,7 +700,6 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
|
|
||||||
.state-badge.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); }
|
.state-badge.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); }
|
||||||
.state-badge.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
.state-badge.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||||
.state-badge.is-delegated { color: #d8b4fe; background: rgba(168,85,247,.12); border-color: rgba(168,85,247,.25); }
|
|
||||||
.state-badge.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); }
|
.state-badge.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); }
|
||||||
.state-badge.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); }
|
.state-badge.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); }
|
||||||
.state-badge.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
.state-badge.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||||
|
|||||||
+2
-1
@@ -86,7 +86,8 @@
|
|||||||
|
|
||||||
- 2026-06-20: Nexus-Auth-Persistenz live verifiziert. Owner-Passwort in der produktiven Postgres-DB geprüft, Stack vollständig neu gestartet und anschließend `postgres`, `api` und `web` per `docker:cli compose up -d --force-recreate` neu erstellt, ohne das DB-Volume zu löschen. Ergebnis: `/health/live` blieb healthy, der Passwort-Hash für `vmbao62@hotmail.de` blieb vor und nach Restart/Recreate identisch. Wichtiges Learning: temporäre Passwörter oder Auth-Fixes niemals an Bao weitergeben, bevor der echte Live-Login oder mindestens der persistierte DB-Hash auf dem Zielstack verifiziert ist.
|
- 2026-06-20: Nexus-Auth-Persistenz live verifiziert. Owner-Passwort in der produktiven Postgres-DB geprüft, Stack vollständig neu gestartet und anschließend `postgres`, `api` und `web` per `docker:cli compose up -d --force-recreate` neu erstellt, ohne das DB-Volume zu löschen. Ergebnis: `/health/live` blieb healthy, der Passwort-Hash für `vmbao62@hotmail.de` blieb vor und nach Restart/Recreate identisch. Wichtiges Learning: temporäre Passwörter oder Auth-Fixes niemals an Bao weitergeben, bevor der echte Live-Login oder mindestens der persistierte DB-Hash auf dem Zielstack verifiziert ist.
|
||||||
- 2026-06-20: Task Board um klickbare Linear-inspirierte Detailansicht erweitert: Board-Karten öffnen jetzt ein strukturiertes Side/Overlay-Detailpanel mit editierbarem Titel, Beschreibung, Status, Priorität, Zuständigkeit und Fälligkeitsdatum sowie geladener Aktivität und Unteraufgaben. `frontend/src/views/TaskBoardView.vue` und `frontend/src/stores/tasks.ts` angepasst. Verifiziert mit `COREPACK_HOME=$PWD/.corepack-home PNPM_HOME=$PWD/.pnpm-home pnpm build`.
|
- 2026-06-20: Task Board um klickbare Linear-inspirierte Detailansicht erweitert: Board-Karten öffnen jetzt ein strukturiertes Side/Overlay-Detailpanel mit editierbarem Titel, Beschreibung, Status, Priorität, Zuständigkeit und Fälligkeitsdatum sowie geladener Aktivität und Unteraufgaben. `frontend/src/views/TaskBoardView.vue` und `frontend/src/stores/tasks.ts` angepasst. Verifiziert mit `COREPACK_HOME=$PWD/.corepack-home PNPM_HOME=$PWD/.pnpm-home pnpm build`.
|
||||||
- 2026-06-19: Task-Board-Doku-Drift behoben: Header-Kommentar in TaskBoardView.vue von "4 columns" auf "6 columns" (Offen, InBearbeitung, Delegiert, Review, Blockiert, Erledigt) korrigiert. tasks.ts-Store-Kopfkommentar um delegated ergänzt.
|
- 2026-06-21: Delegated-/unsichtbares Delegationsmodell auf sichtbaren Parent-Child-Flow umgestellt. `Delegated` aus Board-State, DTOs, Frontend-Spalten und Tests entfernt; Parent-Tasks bleiben bei delegierter Agentenarbeit `In progress`, Child-Tasks laufen sichtbar mit normalen States und `parentTaskId`. Doku in README, Phase 3, Changelog und Controller-Kommentaren angepasst.
|
||||||
|
- 2026-06-19: Task-Board-Doku-Drift behoben; später im Parent-Child-Flow wieder auf 5 sichtbare States ohne separate Delegiert-Spalte umgestellt.
|
||||||
- 2026-06-19: Veralteter TODO.md-Import entfernt: `ImportFromIrisTodoAsync` in TaskService.cs, ITaskService.cs und der import-from-iris-todo-API-Endpoint in DashboardController.cs gelöscht. ImportResultDto aus Models/Dashboard.cs entfernt. TODO.md ist abgeschafft, Task Board alleinige Quelle.
|
- 2026-06-19: Veralteter TODO.md-Import entfernt: `ImportFromIrisTodoAsync` in TaskService.cs, ITaskService.cs und der import-from-iris-todo-API-Endpoint in DashboardController.cs gelöscht. ImportResultDto aus Models/Dashboard.cs entfernt. TODO.md ist abgeschafft, Task Board alleinige Quelle.
|
||||||
- 2026-06-19: Backend-Tests erweitert: TaskBoardTests.cs (69 Tests total, +13 neue) decken TaskStateHelper-BoardGroupKey/ToState/BoardGroupToState/DisplayString/AllStates/IsValidState/IsInProgressOrBlocked/IsDoneOrBacklog ab. Backend-Build 0 Errors, Frontend vue-tsc 0 Errors.
|
- 2026-06-19: Backend-Tests erweitert: TaskBoardTests.cs (69 Tests total, +13 neue) decken TaskStateHelper-BoardGroupKey/ToState/BoardGroupToState/DisplayString/AllStates/IsValidState/IsInProgressOrBlocked/IsDoneOrBacklog ab. Backend-Build 0 Errors, Frontend vue-tsc 0 Errors.
|
||||||
- 2026-06-16: Program.cs refactored: DI extrahiert in `Extensions/ServiceCollectionExtensions.cs`, Middleware in `Extensions/ApplicationBuilderExtensions.cs`, Helpers in `Helpers/PasswordHelper.cs`. Program.cs von ~200 auf 26 Zeilen reduziert.
|
- 2026-06-16: Program.cs refactored: DI extrahiert in `Extensions/ServiceCollectionExtensions.cs`, Middleware in `Extensions/ApplicationBuilderExtensions.cs`, Helpers in `Helpers/PasswordHelper.cs`. Program.cs von ~200 auf 26 Zeilen reduziert.
|
||||||
|
|||||||
+7
-3
@@ -1,12 +1,16 @@
|
|||||||
# Phase 3
|
# Phase 3
|
||||||
|
|
||||||
> Letzte Aktualisierung: 2026-06-08
|
> Letzte Aktualisierung: 2026-06-21
|
||||||
|
|
||||||
- [ ] Office View
|
- [ ] Office View
|
||||||
- [ ] Kalender
|
- [ ] Kalender
|
||||||
- [ ] Agent Collaboration Visualisierung
|
- [x] Agent Collaboration Visualisierung
|
||||||
|
- sichtbarer Parent-Child-Flow im Task Board statt unsichtbarer Delegation
|
||||||
|
- Parent bleibt bei Iris in `In progress`, Child-Tasks sind sichtbar ueber `parentTaskId`
|
||||||
|
- Board/UI zeigen Child-Fortschritt auf Karten und im Detailpanel
|
||||||
|
- Ablauf dokumentiert in [`docs/openclaw-task-board-flow.md`](../docs/openclaw-task-board-flow.md)
|
||||||
- [ ] Advanced Reporting
|
- [ ] Advanced Reporting
|
||||||
|
|
||||||
## Fokus
|
## Fokus
|
||||||
|
|
||||||
Phase 3 bleibt bewusst knapp und wird erst beim Start der Arbeit ausgebaut.
|
Parent-Child-Delegation ist im Board umgesetzt. Offene Phase-3-Schwerpunkte sind jetzt Kalender, Office View und spaetere Reporting-/Visualisierungs-Ausbauten.
|
||||||
|
|||||||
Reference in New Issue
Block a user