feat(backend,frontend): tâches planifiées projet — tool MCP idea_scheduled_task_* et menu utilisateur #305, fix transcript lié indisponible après reboot #314 (QA verte)
#305: nouvel agrégat durable ScheduledProjectTask dans domain (scheduled_task.rs, ids, ports ScheduledProjectTaskStore), source de vérité <app_data_dir>/scheduled-project-tasks/<projectId>.json, schedules bornés V1 interval/daily/weekly avec fuseau, cycle À approuver/Active/Suspendue avec approbation obligatoire des tâches demandées par agent; use cases application/scheduled_task (list/create/update/delete/run_now/reconcile/fire) réutilisant BackgroundTask pour les runs avec distinction planifié/manuel; store infrastructure dédié + scheduler reconcile au boot sans rattrapage en rafale, cwd confiné au projet; 7 tools MCP idea_scheduled_task_* (list/read lecture seule, create/update/delete/run_now/set_enabled write-actions soumises aux permissions natives); commandes Tauri + routes HTTP aux contrats DTO identiques (camelCase nextRunAtMs/lastRunAtMs/lastTaskId/createdBy/updatedBy); frontend: gateways Tauri/HTTP/mock + types ports/domain, surface projet 'Tâches planifiées' (liste utilisateur+agent unifiée, création/édition avec récapitulatif avant activation, approbation/refus, exécuter maintenant, suspendre/reprendre, suppression), catalogage surface web (webSurfaceCatalog/viewPlacement/ViewPanelBody/WebWorkspace); docs/scheduled-project-tasks.md et sdk docs/services.md (047c38b: pas un service plugin V1, pas d'émulation via cron système). #314: correction de 'Conversation liée — transcript indisponible' après reboot: restauration du transcript lié côté application (orchestrator service, lifecycle, session_limit, background sink, terminal registry) et côté frontend (adapters agent/http frames/stream/requestResponse, workStateNormalization, CustomAgentChatView avec état et action utile), tests dto_chat/agent_lifecycle/structured_registry_d1/background_completion_sink étendus. QA verte rapportée par QA sur feature/305-scheduled-project-tasks-mcp, sans bloquant, pour #305 et #314. Hors périmètre worktree: tickets non liés, attachments/, scripts/; aucun push.
This commit is contained in:
109
frontend/src/features/scheduled-tasks/useScheduledTasks.ts
Normal file
109
frontend/src/features/scheduled-tasks/useScheduledTasks.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useGateways } from "@/app/di";
|
||||
import type { Agent, BackgroundCompletion, GatewayError, ScheduledProjectTask } from "@/domain";
|
||||
import type { ScheduledTaskDefinitionInput } from "@/ports";
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error && typeof error === "object" && "message" in error
|
||||
? String((error as GatewayError).message)
|
||||
: String(error);
|
||||
}
|
||||
|
||||
export function useScheduledTasks(projectId: string) {
|
||||
const { scheduledTask, workState, agent, system } = useGateways();
|
||||
const [tasks, setTasks] = useState<ScheduledProjectTask[]>([]);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [runs, setRuns] = useState<BackgroundCompletion[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [definitions, roster, state] = await Promise.all([
|
||||
scheduledTask.list(projectId),
|
||||
agent.listAgents(projectId).catch(() => []),
|
||||
workState.getProjectWorkState(projectId).catch(() => null),
|
||||
]);
|
||||
setTasks(definitions);
|
||||
setAgents(roster);
|
||||
setRuns(state?.agents.flatMap((item) => item.backgroundTasks ?? []) ?? []);
|
||||
setError(null);
|
||||
} catch (cause) {
|
||||
setError(message(cause));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [agent, projectId, scheduledTask, workState]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let cancelled = false;
|
||||
void system.onDomainEvent((event) => {
|
||||
if (event.type === "backgroundTaskChanged" && event.projectId === projectId) {
|
||||
void refresh();
|
||||
}
|
||||
}).then((stop) => {
|
||||
if (cancelled) stop();
|
||||
else unsubscribe = stop;
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [projectId, refresh, system]);
|
||||
|
||||
const runsById = useMemo(
|
||||
() => new Map(runs.map((run) => [run.taskId, run])),
|
||||
[runs],
|
||||
);
|
||||
const agentNames = useMemo(
|
||||
() => new Map(agents.map((item) => [item.id, item.name])),
|
||||
[agents],
|
||||
);
|
||||
|
||||
async function mutate(id: string, action: () => Promise<unknown>): Promise<boolean> {
|
||||
setBusyId(id);
|
||||
setError(null);
|
||||
try {
|
||||
await action();
|
||||
await refresh();
|
||||
return true;
|
||||
} catch (cause) {
|
||||
setError(message(cause));
|
||||
return false;
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
loading,
|
||||
busyId,
|
||||
error,
|
||||
runsById,
|
||||
agentNames,
|
||||
refresh,
|
||||
create: (input: ScheduledTaskDefinitionInput) =>
|
||||
mutate("new", () => scheduledTask.create(projectId, input)),
|
||||
update: (task: ScheduledProjectTask, input: ScheduledTaskDefinitionInput) =>
|
||||
mutate(task.id, () => scheduledTask.update(projectId, task.id, task.auditRevision, input)),
|
||||
remove: (task: ScheduledProjectTask) =>
|
||||
mutate(task.id, () => scheduledTask.delete(projectId, task.id)),
|
||||
runNow: (task: ScheduledProjectTask) =>
|
||||
mutate(task.id, () => scheduledTask.runNow(projectId, task.id)),
|
||||
setEnabled: (task: ScheduledProjectTask, enabled: boolean) =>
|
||||
mutate(task.id, () => scheduledTask.setEnabled(projectId, task.id, enabled)),
|
||||
cancelRun: (task: ScheduledProjectTask) =>
|
||||
task.lastTaskId
|
||||
? mutate(task.id, () => workState.cancelBackgroundTask(task.lastTaskId!))
|
||||
: Promise.resolve(undefined),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user