feat(dashboard): task system with DB persistence, CRUD endpoints, frontend API integration
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nexus.Api.Data;
|
||||
using Nexus.Api.Models;
|
||||
using Nexus.Api.Repositories;
|
||||
using Nexus.Api.Services;
|
||||
|
||||
namespace Nexus.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/dashboard")]
|
||||
public class DashboardController(OpenClawGatewayClient gateway, ILogger<DashboardController> logger)
|
||||
public class DashboardController(
|
||||
OpenClawGatewayClient gateway,
|
||||
ITaskRepository taskRepo,
|
||||
IActivityRepository activityRepo,
|
||||
ILogger<DashboardController> logger)
|
||||
: ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -211,8 +217,156 @@ public class DashboardController(OpenClawGatewayClient gateway, ILogger<Dashboar
|
||||
return Ok(models);
|
||||
}
|
||||
|
||||
// ========== Task Endpoints ==========
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-done tasks (status != 'Done'), ordered by creation date descending.
|
||||
/// </summary>
|
||||
[HttpGet("tasks")]
|
||||
public async Task<List<DashboardTaskDto>> GetTasks(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tasks = await taskRepo.GetAllAsync(ct);
|
||||
return tasks
|
||||
.Where(t => !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.Select(MapToDto)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Dashboard tasks fetch failed");
|
||||
return new List<DashboardTaskDto>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task and logs an activity event.
|
||||
/// </summary>
|
||||
[HttpPost("tasks")]
|
||||
public async Task<ActionResult<DashboardTaskDto>> CreateTask(
|
||||
[FromBody] CreateDashboardTaskRequest request, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
return BadRequest(new { error = "Title is required." });
|
||||
|
||||
var task = new WorkTask
|
||||
{
|
||||
Title = request.Title.Trim(),
|
||||
Detail = request.Detail?.Trim(),
|
||||
Source = string.IsNullOrWhiteSpace(request.Source) ? "bao" : request.Source.Trim(),
|
||||
Priority = string.IsNullOrWhiteSpace(request.Priority) ? "Normal" : request.Priority.Trim(),
|
||||
AssignedTo = request.AssignedTo?.Trim(),
|
||||
};
|
||||
|
||||
await taskRepo.AddAsync(task, ct);
|
||||
await activityRepo.AddAsync(new ActivityEvent
|
||||
{
|
||||
Type = "task",
|
||||
Message = $"Task \"{task.Title}\" created ({task.Source})"
|
||||
}, ct);
|
||||
|
||||
return Created($"/api/dashboard/tasks/{task.Id}", MapToDto(task));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing task (title, detail, source, priority, assignedTo).
|
||||
/// </summary>
|
||||
[HttpPut("tasks/{id:guid}")]
|
||||
public async Task<ActionResult<DashboardTaskDto>> UpdateTask(
|
||||
Guid id, [FromBody] UpdateDashboardTaskRequest request, CancellationToken ct)
|
||||
{
|
||||
var task = await taskRepo.GetByIdAsync(id, ct);
|
||||
if (task is null)
|
||||
return NotFound(new { error = "Task not found." });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Title))
|
||||
task.Title = request.Title.Trim();
|
||||
if (request.Detail is not null)
|
||||
task.Detail = string.IsNullOrWhiteSpace(request.Detail) ? null : request.Detail.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(request.Source))
|
||||
task.Source = request.Source.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(request.Priority))
|
||||
task.Priority = request.Priority.Trim();
|
||||
if (request.AssignedTo is not null)
|
||||
task.AssignedTo = string.IsNullOrWhiteSpace(request.AssignedTo) ? null : request.AssignedTo.Trim();
|
||||
|
||||
await taskRepo.UpdateAsync(task, ct);
|
||||
await activityRepo.AddAsync(new ActivityEvent
|
||||
{
|
||||
Type = "task",
|
||||
Message = $"Task \"{task.Title}\" updated"
|
||||
}, ct);
|
||||
|
||||
return Ok(MapToDto(task));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a task (only if status is 'Done' or 'Backlog').
|
||||
/// </summary>
|
||||
[HttpDelete("tasks/{id:guid}")]
|
||||
public async Task<ActionResult> DeleteTask(Guid id, CancellationToken ct)
|
||||
{
|
||||
var task = await taskRepo.GetByIdAsync(id, ct);
|
||||
if (task is null)
|
||||
return NotFound(new { error = "Task not found." });
|
||||
|
||||
if (!TaskStateHelper.IsDoneOrBacklog(task.State))
|
||||
return StatusCode(403, new { error = "Only tasks in 'Done' or 'Backlog' state can be deleted." });
|
||||
|
||||
await activityRepo.AddAsync(new ActivityEvent
|
||||
{
|
||||
Type = "task",
|
||||
Message = $"Task \"{task.Title}\" deleted"
|
||||
}, ct);
|
||||
await taskRepo.DeleteAsync(task, ct);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the status of a task.
|
||||
/// </summary>
|
||||
[HttpPatch("tasks/{id:guid}/status")]
|
||||
public async Task<ActionResult<DashboardTaskDto>> UpdateTaskStatus(
|
||||
Guid id, [FromBody] UpdateDashboardTaskStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!TaskStateHelper.IsValidState(request.Status))
|
||||
return BadRequest(new { error = $"Unsupported status: '{request.Status}'. Valid: {string.Join(", ", TaskStateHelper.AllStates)}" });
|
||||
|
||||
var task = await taskRepo.GetByIdAsync(id, ct);
|
||||
if (task is null)
|
||||
return NotFound(new { error = "Task not found." });
|
||||
|
||||
var canonicalState = TaskStateHelper.AllStates.First(s =>
|
||||
s.Equals(request.Status, StringComparison.OrdinalIgnoreCase));
|
||||
task.State = canonicalState;
|
||||
|
||||
await taskRepo.UpdateAsync(task, ct);
|
||||
await activityRepo.AddAsync(new ActivityEvent
|
||||
{
|
||||
Type = "task",
|
||||
Message = $"Task \"{task.Title}\" → {canonicalState}"
|
||||
}, ct);
|
||||
|
||||
return Ok(MapToDto(task));
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
|
||||
private static DashboardTaskDto MapToDto(WorkTask t) => new(
|
||||
t.Id,
|
||||
t.Title,
|
||||
t.Detail,
|
||||
t.Source,
|
||||
t.State,
|
||||
t.Priority,
|
||||
t.AssignedTo,
|
||||
t.CreatedAt,
|
||||
t.UpdatedAt
|
||||
);
|
||||
|
||||
private static DateTimeOffset ParseTimestamp(string timestamp)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt))
|
||||
|
||||
@@ -77,9 +77,13 @@ public sealed class WorkTask
|
||||
{
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
public required string Title { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public string State { get; set; } = "Backlog";
|
||||
public string Priority { get; set; } = "Normal";
|
||||
public string Source { get; set; } = "bao";
|
||||
public string? AssignedTo { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Nexus.Api.Data;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nexus.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(NexusDbContext))]
|
||||
[Migration("20260611154800_AddTaskDetailFields")]
|
||||
partial class AddTaskDetailFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.ActivityEvent", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Activity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.NexusUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastLoginAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FamilyId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ReplacedByTokenHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTimeOffset?>("RevokedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId", "FamilyId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AssignedTo")
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Detail")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(240)
|
||||
.HasColumnType("character varying(240)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssignedTo");
|
||||
|
||||
b.HasIndex("Source");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("Nexus.Api.Data.NexusUser", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nexus.Api.Data.NexusUser", b =>
|
||||
{
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nexus.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTaskDetailFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AssignedTo",
|
||||
table: "Tasks",
|
||||
type: "character varying(60)",
|
||||
maxLength: 60,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CreatedAt",
|
||||
table: "Tasks",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValueSql: "NOW()");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Detail",
|
||||
table: "Tasks",
|
||||
type: "character varying(2000)",
|
||||
maxLength: 2000,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Source",
|
||||
table: "Tasks",
|
||||
type: "character varying(60)",
|
||||
maxLength: 60,
|
||||
nullable: false,
|
||||
defaultValue: "bao");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_AssignedTo",
|
||||
table: "Tasks",
|
||||
column: "AssignedTo");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_Source",
|
||||
table: "Tasks",
|
||||
column: "Source");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Tasks_AssignedTo",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Tasks_Source",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AssignedTo",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Detail",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Source",
|
||||
table: "Tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,17 @@ namespace Nexus.Api.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AssignedTo")
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Detail")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -179,6 +190,11 @@ namespace Nexus.Api.Migrations
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -193,6 +209,10 @@ namespace Nexus.Api.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssignedTo");
|
||||
|
||||
b.HasIndex("Source");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,15 @@ public sealed class NexusDbContext(DbContextOptions<NexusDbContext> options) : D
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Project>().Property(x => x.Name).HasMaxLength(160);
|
||||
modelBuilder.Entity<WorkTask>().Property(x => x.Title).HasMaxLength(240);
|
||||
modelBuilder.Entity<WorkTask>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Title).HasMaxLength(240);
|
||||
entity.Property(x => x.Detail).HasMaxLength(2000);
|
||||
entity.Property(x => x.Source).HasMaxLength(60);
|
||||
entity.Property(x => x.AssignedTo).HasMaxLength(60);
|
||||
entity.HasIndex(x => x.Source);
|
||||
entity.HasIndex(x => x.AssignedTo);
|
||||
});
|
||||
modelBuilder.Entity<ActivityEvent>().Property(x => x.Message).HasMaxLength(1000);
|
||||
modelBuilder.Entity<NexusUser>().HasIndex(u => u.NormalizedEmail).IsUnique();
|
||||
modelBuilder.Entity<RefreshToken>().HasIndex(r => r.TokenHash).IsUnique();
|
||||
|
||||
@@ -62,3 +62,37 @@ public sealed record ModelOption(
|
||||
string Name,
|
||||
string Provider
|
||||
);
|
||||
|
||||
// ── Dashboard Task DTOs ──
|
||||
|
||||
public sealed record DashboardTaskDto(
|
||||
Guid Id,
|
||||
string Title,
|
||||
string? Detail,
|
||||
string Source,
|
||||
string State,
|
||||
string Priority,
|
||||
string? AssignedTo,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt
|
||||
);
|
||||
|
||||
public sealed record CreateDashboardTaskRequest(
|
||||
string Title,
|
||||
string? Detail,
|
||||
string? Source,
|
||||
string? Priority,
|
||||
string? AssignedTo
|
||||
);
|
||||
|
||||
public sealed record UpdateDashboardTaskRequest(
|
||||
string? Title,
|
||||
string? Detail,
|
||||
string? Source,
|
||||
string? Priority,
|
||||
string? AssignedTo
|
||||
);
|
||||
|
||||
public sealed record UpdateDashboardTaskStatusRequest(
|
||||
string Status
|
||||
);
|
||||
|
||||
@@ -103,6 +103,18 @@ interface DashboardQueueItem {
|
||||
status: string
|
||||
}
|
||||
|
||||
interface DashboardTaskResponse {
|
||||
id: string
|
||||
title: string
|
||||
detail: string | null
|
||||
source: string
|
||||
state: string
|
||||
priority: string
|
||||
assignedTo: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// ── Agent Catalog (static enrichment) ──
|
||||
|
||||
const AGENT_CATALOG: Record<string, Partial<AgentNodeData>> = {
|
||||
@@ -244,12 +256,8 @@ const busySince = ref(0)
|
||||
// Operations Feed
|
||||
const feedEntries = ref<FeedEntry[]>([])
|
||||
|
||||
// Open Tasks (mock only – no API endpoint)
|
||||
const openTasks = ref<OpenTask[]>([
|
||||
{ id: 't1', title: 'Agent Thinking Panel visualisieren', detail: 'Live-Animation der Denkprozesse im AgentModal', source: 'iris', createdAt: '22:30' },
|
||||
{ id: 't2', title: 'CI/CD Pipeline Monitoring Dashboard', detail: 'Echtzeit-Status der Gitea Actions im Dashboard', source: 'iris', createdAt: '21:15' },
|
||||
{ id: 't3', title: 'Dungeon System Dokumentation', detail: 'API-Doku für Room-Generation-Endpunkte schreiben', source: 'bao', createdAt: '20:00' },
|
||||
])
|
||||
// Open Tasks (fetched from API)
|
||||
const openTasks = ref<OpenTask[]>([])
|
||||
|
||||
// Queue
|
||||
const queue = ref<QueueItem[]>([])
|
||||
@@ -342,6 +350,45 @@ async function fetchQueue(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTasks(): Promise<void> {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/tasks')
|
||||
if (!res.ok) return
|
||||
const data: DashboardTaskResponse[] = await res.json()
|
||||
openTasks.value = data.map(mapTaskResponse)
|
||||
} catch {
|
||||
// API unreachable – keep current values
|
||||
}
|
||||
}
|
||||
|
||||
function mapTaskResponse(t: DashboardTaskResponse): OpenTask {
|
||||
const source: OpenTask['source'] = t.source === 'iris' ? 'iris' : 'bao'
|
||||
// Format createdAt as relative time string (like "22:30")
|
||||
const created = new Date(t.createdAt)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - created.getTime()
|
||||
const diffMins = Math.floor(diffMs / 60000)
|
||||
|
||||
let createdAt: string
|
||||
if (diffMins < 1) {
|
||||
createdAt = 'just now'
|
||||
} else if (diffMins < 60) {
|
||||
createdAt = `${diffMins}m`
|
||||
} else if (diffMins < 1440) {
|
||||
createdAt = `${Math.floor(diffMins / 60)}h`
|
||||
} else {
|
||||
createdAt = created.toLocaleDateString('de-DE', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
return {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
detail: t.detail ?? '',
|
||||
source,
|
||||
createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chat Send ──
|
||||
|
||||
async function sendChatMessage(text: string): Promise<void> {
|
||||
@@ -454,6 +501,7 @@ function startPolling(): void {
|
||||
fetchOperations()
|
||||
fetchChatMessages()
|
||||
fetchQueue()
|
||||
fetchTasks()
|
||||
|
||||
// Polling intervals
|
||||
intervals.push(setInterval(fetchStatus, 5000))
|
||||
@@ -461,6 +509,7 @@ function startPolling(): void {
|
||||
intervals.push(setInterval(fetchOperations, 10000))
|
||||
intervals.push(setInterval(fetchChatMessages, 3000))
|
||||
intervals.push(setInterval(fetchQueue, 10000))
|
||||
intervals.push(setInterval(fetchTasks, 15000))
|
||||
}
|
||||
|
||||
function stopPolling(): void {
|
||||
@@ -512,5 +561,6 @@ export function useDashboardData() {
|
||||
fetchOperations,
|
||||
fetchChatMessages,
|
||||
fetchQueue,
|
||||
fetchTasks,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user