Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45c6b24928 | |||
| 5fb62bef8a |
@@ -135,14 +135,52 @@ public class DashboardController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the cron queue / pending tasks.
|
/// Returns aggregated queue: cron jobs + open tasks (merged, sorted by priority).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("queue")]
|
[HttpGet("queue")]
|
||||||
public async Task<List<QueueItem>> GetQueue()
|
public async Task<List<QueueItem>> GetQueue(CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await gateway.GetQueueAsync();
|
// Fetch cron jobs and open tasks concurrently
|
||||||
|
var cronTask = gateway.GetQueueAsync();
|
||||||
|
var tasksTask = taskRepo.GetAllAsync(ct);
|
||||||
|
|
||||||
|
await Task.WhenAll(cronTask, tasksTask);
|
||||||
|
|
||||||
|
var cronJobs = cronTask.Result;
|
||||||
|
var openTasks = tasksTask.Result
|
||||||
|
.Where(t => !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var merged = new List<QueueItem>();
|
||||||
|
|
||||||
|
// Map cron jobs (already in QueueItem format from gateway)
|
||||||
|
merged.AddRange(cronJobs);
|
||||||
|
|
||||||
|
// Map open tasks to QueueItems
|
||||||
|
foreach (var t in openTasks)
|
||||||
|
{
|
||||||
|
var priority = NormalizePriority(t.Priority);
|
||||||
|
merged.Add(new QueueItem(
|
||||||
|
"task-" + t.Id.ToString(),
|
||||||
|
t.Title,
|
||||||
|
t.State,
|
||||||
|
priority,
|
||||||
|
"task",
|
||||||
|
"--"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: high priority first, then medium, then low
|
||||||
|
var priorityOrder = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["high"] = 0,
|
||||||
|
["medium"] = 1,
|
||||||
|
["low"] = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
return merged.OrderBy(q => priorityOrder.GetValueOrDefault(q.Priority, 99)).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -151,6 +189,115 @@ public class DashboardController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizePriority(string priority)
|
||||||
|
{
|
||||||
|
return priority.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"high" or "critical" or "urgent" => "high",
|
||||||
|
"low" or "minor" => "low",
|
||||||
|
_ => "medium"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a queue item: cron jobs are deleted via gateway, tasks are set to Done.
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("queue/{id}")]
|
||||||
|
public async Task<ActionResult> DeleteQueueItem(string id, [FromQuery] string? source, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.Equals(source, "cron", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var ok = await gateway.DeleteCronJobAsync(id);
|
||||||
|
if (!ok)
|
||||||
|
return StatusCode(502, new { error = "Gateway could not delete cron job" });
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
else if (string.Equals(source, "task", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Extract the actual GUID from the prefixed id ("task-{guid}")
|
||||||
|
if (!id.StartsWith("task-"))
|
||||||
|
return BadRequest(new { error = "Invalid task id format" });
|
||||||
|
|
||||||
|
var guidStr = id["task-".Length..];
|
||||||
|
if (!Guid.TryParse(guidStr, out var guid))
|
||||||
|
return BadRequest(new { error = "Invalid task id" });
|
||||||
|
|
||||||
|
var task = await taskRepo.GetByIdAsync(guid, ct);
|
||||||
|
if (task is null)
|
||||||
|
return NotFound(new { error = "Task not found" });
|
||||||
|
|
||||||
|
// Set task status to Done instead of deleting
|
||||||
|
task.State = "Done";
|
||||||
|
await taskRepo.UpdateAsync(task, ct);
|
||||||
|
await activityRepo.AddAsync(new ActivityEvent
|
||||||
|
{
|
||||||
|
Type = "task",
|
||||||
|
Message = $"Task \"{task.Title}\" completed via queue"
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: try cron
|
||||||
|
var deleted = await gateway.DeleteCronJobAsync(id);
|
||||||
|
if (!deleted)
|
||||||
|
return NotFound(new { error = "Queue item not found" });
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Delete queue item failed for {Id}", id);
|
||||||
|
return StatusCode(500, new { error = "Internal error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the priority of a queue item (only for tasks; cron jobs are ignored).
|
||||||
|
/// Cycles: high → medium → low → high.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("queue/{id}/priority")]
|
||||||
|
public async Task<ActionResult> ChangeQueuePriority(string id, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!id.StartsWith("task-"))
|
||||||
|
return Ok(new { status = "ignored", reason = "Cron job priorities are managed by the gateway" });
|
||||||
|
|
||||||
|
var guidStr = id["task-".Length..];
|
||||||
|
if (!Guid.TryParse(guidStr, out var guid))
|
||||||
|
return BadRequest(new { error = "Invalid task id" });
|
||||||
|
|
||||||
|
var task = await taskRepo.GetByIdAsync(guid, ct);
|
||||||
|
if (task is null)
|
||||||
|
return NotFound(new { error = "Task not found" });
|
||||||
|
|
||||||
|
// Cycle priority: high → medium → low → high
|
||||||
|
task.Priority = task.Priority.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"high" => "Medium",
|
||||||
|
"medium" => "Low",
|
||||||
|
"low" => "High",
|
||||||
|
_ => "Medium"
|
||||||
|
};
|
||||||
|
|
||||||
|
await taskRepo.UpdateAsync(task, ct);
|
||||||
|
await activityRepo.AddAsync(new ActivityEvent
|
||||||
|
{
|
||||||
|
Type = "task",
|
||||||
|
Message = $"Task \"{task.Title}\" priority → {task.Priority}"
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return Ok(new { status = "ok", priority = task.Priority });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Change queue priority failed for {Id}", id);
|
||||||
|
return StatusCode(500, new { error = "Internal error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the current model and provider for a specific agent session.
|
/// Returns the current model and provider for a specific agent session.
|
||||||
/// Calls session_status with the agent's session key.
|
/// Calls session_status with the agent's session key.
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ public sealed record DashboardStatus(
|
|||||||
public sealed record QueueItem(
|
public sealed record QueueItem(
|
||||||
string Id,
|
string Id,
|
||||||
string Name,
|
string Name,
|
||||||
string Status
|
string Status,
|
||||||
|
string Priority,
|
||||||
|
string Source,
|
||||||
|
string WaitTime
|
||||||
);
|
);
|
||||||
|
|
||||||
public sealed record AgentModelInfo(
|
public sealed record AgentModelInfo(
|
||||||
|
|||||||
@@ -639,7 +639,28 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
|||||||
var status = j["state"]?["lastStatus"]?.GetValue<string>()
|
var status = j["state"]?["lastStatus"]?.GetValue<string>()
|
||||||
?? j["status"]?.GetValue<string>()
|
?? j["status"]?.GetValue<string>()
|
||||||
?? "unknown";
|
?? "unknown";
|
||||||
items.Add(new QueueItem(id, name, status));
|
|
||||||
|
// Calculate waitTime from nextRun if available
|
||||||
|
var waitTime = "--";
|
||||||
|
var nextRunStr = j["nextRun"]?.GetValue<string>()
|
||||||
|
?? j["next_run"]?.GetValue<string>()
|
||||||
|
?? j["scheduledAt"]?.GetValue<string>();
|
||||||
|
if (nextRunStr is not null && DateTimeOffset.TryParse(nextRunStr, out var nextRun))
|
||||||
|
{
|
||||||
|
var diff = nextRun - DateTimeOffset.UtcNow;
|
||||||
|
if (diff.TotalMinutes < 0)
|
||||||
|
waitTime = "now";
|
||||||
|
else if (diff.TotalMinutes < 1)
|
||||||
|
waitTime = "<1m";
|
||||||
|
else if (diff.TotalMinutes < 60)
|
||||||
|
waitTime = $"{(int)diff.TotalMinutes}m";
|
||||||
|
else if (diff.TotalHours < 24)
|
||||||
|
waitTime = $"{(int)diff.TotalHours}h";
|
||||||
|
else
|
||||||
|
waitTime = $"{(int)diff.TotalDays}d";
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new QueueItem(id, name, status, "medium", "cron", waitTime));
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -649,6 +670,19 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteCronJobAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await InvokeToolAsync("cron", new { action = "delete", id });
|
||||||
|
return result is not null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<DashboardStatus> GetStatusAsync()
|
public async Task<DashboardStatus> GetStatusAsync()
|
||||||
{
|
{
|
||||||
var gatewayOk = false;
|
var gatewayOk = false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ArrowDown,
|
ArrowDown,
|
||||||
Trash2,
|
Trash2,
|
||||||
Zap,
|
Zap,
|
||||||
|
RefreshCw,
|
||||||
} from '@lucide/vue'
|
} from '@lucide/vue'
|
||||||
import type { QueueItem } from '../../composables/useDashboardData'
|
import type { QueueItem } from '../../composables/useDashboardData'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
@@ -107,6 +108,7 @@ function onDragEnd(): void {
|
|||||||
>
|
>
|
||||||
<div class="queue-item-body">
|
<div class="queue-item-body">
|
||||||
<div class="queue-item-head">
|
<div class="queue-item-head">
|
||||||
|
<span class="queue-source-badge" :class="item.source">{{ item.source }}</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="text-[7px] font-bold uppercase tracking-wider py-0 px-1.5 border"
|
class="text-[7px] font-bold uppercase tracking-wider py-0 px-1.5 border"
|
||||||
@@ -124,8 +126,8 @@ function onDragEnd(): void {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="queue-actions">
|
<div class="queue-actions">
|
||||||
<Button variant="ghost" size="icon" class="h-5 w-5 text-[#6b7385] hover:text-[#e8eaf0]" title="Execute now" @click.stop="emit('executeNow', item.id)">
|
<Button variant="ghost" size="icon" class="h-5 w-5 text-[#6b7385] hover:text-[#e8eaf0]" title="Cycle priority" @click.stop="emit('changePriority', item.id, item.priority === 'high' ? 'medium' : item.priority === 'medium' ? 'low' : 'high')">
|
||||||
<Zap :size="12" />
|
<RefreshCw :size="12" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-5 w-5 text-[#6b7385] hover:text-[#e8eaf0]" title="Move up" :disabled="idx === 0" @click.stop="emit('moveUp', item.id)">
|
<Button variant="ghost" size="icon" class="h-5 w-5 text-[#6b7385] hover:text-[#e8eaf0]" title="Move up" :disabled="idx === 0" @click.stop="emit('moveUp', item.id)">
|
||||||
<ArrowUp :size="12" />
|
<ArrowUp :size="12" />
|
||||||
@@ -257,6 +259,27 @@ function onDragEnd(): void {
|
|||||||
color: #6b7385;
|
color: #6b7385;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Source badge */
|
||||||
|
.queue-source-badge {
|
||||||
|
font-size: 7px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
.queue-source-badge.cron {
|
||||||
|
color: #22c55e;
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
.queue-source-badge.task {
|
||||||
|
color: #a78bfa;
|
||||||
|
background: rgba(167, 139, 250, 0.1);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* Transition */
|
/* Transition */
|
||||||
.queue-expand-enter-active,
|
.queue-expand-enter-active,
|
||||||
.queue-expand-leave-active {
|
.queue-expand-leave-active {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface QueueItem {
|
|||||||
text: string
|
text: string
|
||||||
priority: 'high' | 'medium' | 'low'
|
priority: 'high' | 'medium' | 'low'
|
||||||
waitTime: string
|
waitTime: string
|
||||||
|
source: 'cron' | 'task'
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API Response Interfaces ──
|
// ── API Response Interfaces ──
|
||||||
@@ -101,6 +102,9 @@ interface DashboardQueueItem {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
status: string
|
status: string
|
||||||
|
priority: string
|
||||||
|
source: string
|
||||||
|
waitTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardTaskResponse {
|
interface DashboardTaskResponse {
|
||||||
@@ -340,10 +344,11 @@ async function fetchQueue(): Promise<void> {
|
|||||||
queue.value = data.map((item) => ({
|
queue.value = data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
text: item.name,
|
text: item.name,
|
||||||
priority: (item.status === 'high' || item.status === 'medium' || item.status === 'low')
|
priority: (item.priority === 'high' || item.priority === 'medium' || item.priority === 'low')
|
||||||
? item.status as 'high' | 'medium' | 'low'
|
? item.priority as 'high' | 'medium' | 'low'
|
||||||
: 'medium',
|
: 'medium',
|
||||||
waitTime: '--',
|
waitTime: item.waitTime || '--',
|
||||||
|
source: (item.source === 'cron' || item.source === 'task') ? item.source as 'cron' | 'task' : 'cron',
|
||||||
}))
|
}))
|
||||||
} catch {
|
} catch {
|
||||||
// API unreachable – keep current values
|
// API unreachable – keep current values
|
||||||
@@ -443,9 +448,24 @@ async function sendChatMessage(text: string): Promise<void> {
|
|||||||
|
|
||||||
// ── Queue Operations ──
|
// ── Queue Operations ──
|
||||||
|
|
||||||
function removeQueueItem(id: string): void {
|
async function removeQueueItem(id: string): Promise<void> {
|
||||||
const idx = queue.value.findIndex(q => q.id === id)
|
const item = queue.value.find(q => q.id === id)
|
||||||
if (idx !== -1) queue.value.splice(idx, 1)
|
if (!item) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`/api/dashboard/queue/${encodeURIComponent(id)}?source=${item.source}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn('[Dashboard] Failed to remove queue item', id, res.status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Remove from local state on success
|
||||||
|
const idx = queue.value.findIndex(q => q.id === id)
|
||||||
|
if (idx !== -1) queue.value.splice(idx, 1)
|
||||||
|
} catch {
|
||||||
|
console.warn('[Dashboard] Error removing queue item', id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveQueueItem(fromIdx: number, toIdx: number): void {
|
function moveQueueItem(fromIdx: number, toIdx: number): void {
|
||||||
@@ -454,9 +474,40 @@ function moveQueueItem(fromIdx: number, toIdx: number): void {
|
|||||||
queue.value.splice(toIdx, 0, item)
|
queue.value.splice(toIdx, 0, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeQueuePriority(id: string, priority: QueueItem['priority']): void {
|
async function changeQueuePriority(id: string, priority: QueueItem['priority']): Promise<void> {
|
||||||
const item = queue.value.find(q => q.id === id)
|
const item = queue.value.find(q => q.id === id)
|
||||||
if (item) item.priority = priority
|
if (!item) return
|
||||||
|
|
||||||
|
// For cron jobs, just update locally (gateway manages its own priorities)
|
||||||
|
if (item.source === 'cron') {
|
||||||
|
item.priority = priority
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`/api/dashboard/queue/${encodeURIComponent(id)}/priority`, {
|
||||||
|
method: 'PUT',
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn('[Dashboard] Failed to change priority', id, res.status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Update priority from API response
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.priority) {
|
||||||
|
const normalized = data.priority.toLowerCase()
|
||||||
|
if (normalized === 'high' || normalized === 'medium' || normalized === 'low') {
|
||||||
|
item.priority = normalized as 'high' | 'medium' | 'low'
|
||||||
|
} else {
|
||||||
|
// Fallback: cycle locally
|
||||||
|
item.priority = priority
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.priority = priority
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('[Dashboard] Error changing priority', id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Runtime ──
|
// ── Runtime ──
|
||||||
|
|||||||
Reference in New Issue
Block a user