feat(rails): GET /api/sub-tasks/:id — node detail with parents/events

This commit is contained in:
2026-04-10 17:54:55 +09:00
parent e2d71ca47d
commit 9aeef223c6
5 changed files with 78 additions and 0 deletions

View File

View File

@@ -127,6 +127,74 @@ export async function recordSubTaskEvent(
}
}
function tryParseJson(s: string): unknown {
try {
return JSON.parse(s);
} catch {
return s;
}
}
export async function getSubTaskDetail(id: string): Promise<unknown | null> {
const prisma = getPrisma();
const node = await prisma.subTask.findUnique({
where: { id },
include: {
events: {
orderBy: { timestamp: "asc" },
select: {
id: true,
eventType: true,
payloadJson: true,
timestamp: true,
},
},
},
});
if (!node) return null;
// Walk up parent chain
const parents: Array<{ id: string; role: string; title: string }> = [];
let cursor: string | null = node.parentId;
while (cursor) {
const p = await prisma.subTask.findUnique({
where: { id: cursor },
select: { id: true, parentId: true, role: true, title: true },
});
if (!p) break;
parents.unshift({ id: p.id, role: p.role, title: p.title });
cursor = p.parentId;
}
// Direct children list
const children = await prisma.subTask.findMany({
where: { parentId: id },
orderBy: { createdAt: "asc" },
select: {
id: true,
role: true,
agentName: true,
title: true,
state: true,
model: true,
startedAt: true,
completedAt: true,
},
});
return {
...node,
parents,
childrenList: children,
events: node.events.map((e) => ({
id: e.id,
eventType: e.eventType,
payload: tryParseJson(e.payloadJson),
timestamp: e.timestamp,
})),
};
}
export async function getSubTaskTree(pipelineId: string): Promise<unknown> {
const prisma = getPrisma();
const all = await prisma.subTask.findMany({

View File

@@ -18,6 +18,7 @@ import {
recordSubTaskEvent,
updateSubTask,
getSubTaskTree,
getSubTaskDetail,
} from "../hierarchy/store.js";
import { childLogger } from "../logger.js";
@@ -191,6 +192,15 @@ export async function startHttpServer(opts: ServerOpts): Promise<{
return sendJson(res, 200, { pipelineId: pid, tree });
}
// ── Single sub-task detail ──
const detailMatch = path.match(/^\/api\/sub-tasks\/([^/]+)$/);
if (method === "GET" && detailMatch) {
const id = detailMatch[1]!;
const detail = await getSubTaskDetail(id);
if (!detail) return sendJson(res, 404, { error: "not_found" });
return sendJson(res, 200, detail);
}
return sendJson(res, 404, { error: "not_found", path });
} catch (err) {
log.error(