95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { existsSync, readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { type Repository, type Job, localTaskRepoName } from '../db/repository.js';
|
|
import { logger } from '../logger.js';
|
|
import { canViewTask } from './local-api-helpers.js';
|
|
|
|
const MAX_ACTIVITY_LOG_CHARS = 4000;
|
|
|
|
function readActivityLog(worktreePath: string | null, maxChars: number = 0): string {
|
|
if (!worktreePath) return '';
|
|
const logPath = join(worktreePath, 'logs', 'activity.log');
|
|
if (!existsSync(logPath)) return '';
|
|
try {
|
|
const content = readFileSync(logPath, 'utf-8');
|
|
return maxChars > 0 && content.length > maxChars
|
|
? content.slice(-maxChars)
|
|
: content;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function createSubtaskActivityRouter(repo: Repository): Router {
|
|
const router = Router();
|
|
|
|
// GET /:id/subtasks/activities — bulk fetch all subtask activities (includes nested subtasks)
|
|
router.get('/:id/subtasks/activities', async (req: Request, res: Response) => {
|
|
try {
|
|
const taskId = Number(req.params.id);
|
|
const viewer = req.user as Express.User | undefined;
|
|
const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
|
|
if (!canViewTask(req, res, task)) return;
|
|
|
|
const latestJob = await repo.getLatestJobForIssue(localTaskRepoName(taskId), taskId);
|
|
if (!latestJob) { res.status(404).json({ error: 'No job found' }); return; }
|
|
|
|
// 再帰的に全サブジョブ(孫含む)を収集
|
|
const collectAllSubJobs = async (parentId: string): Promise<Job[]> => {
|
|
const jobs = await repo.getSubJobs(parentId);
|
|
const result = [...jobs];
|
|
for (const job of jobs) {
|
|
if (job.status === 'waiting_subtasks') {
|
|
result.push(...await collectAllSubJobs(job.id));
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
const allJobs = await collectAllSubJobs(latestJob.id);
|
|
|
|
const subtasks = allJobs.map(job => ({
|
|
jobId: job.id,
|
|
issueNumber: job.issueNumber,
|
|
status: job.status,
|
|
currentMovement: job.currentMovement ?? null,
|
|
currentActivity: job.currentActivity ?? null,
|
|
activityLog: readActivityLog(job.worktreePath, MAX_ACTIVITY_LOG_CHARS),
|
|
}));
|
|
|
|
res.json({ subtasks });
|
|
} catch (err) {
|
|
logger.error(`Subtask activities API error: ${err}`);
|
|
res.status(500).json({ error: 'Failed to fetch subtask activities' });
|
|
}
|
|
});
|
|
|
|
// GET /:id/subtasks/:jobId/activity — individual subtask activity (supports nested subtasks)
|
|
router.get('/:id/subtasks/:jobId/activity', async (req: Request, res: Response) => {
|
|
try {
|
|
const taskId = Number(req.params.id);
|
|
const jobId = req.params.jobId;
|
|
|
|
const viewer = (req.user as Express.User | undefined) ?? undefined;
|
|
const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
|
|
if (!canViewTask(req, res, task)) return;
|
|
|
|
// jobId で直接取得(孫タスクにも対応)
|
|
const job = await repo.getJob(jobId, viewer ? { viewer } : undefined);
|
|
if (!job || !job.worktreePath) { res.status(404).json({ error: 'Subtask not found' }); return; }
|
|
|
|
// タスクのワークスペース配下であることを確認
|
|
if (task!.workspacePath && !job.worktreePath.startsWith(task!.workspacePath)) {
|
|
res.status(404).json({ error: 'Subtask not found' }); return;
|
|
}
|
|
|
|
res.json({ activityLog: readActivityLog(job.worktreePath) });
|
|
} catch (err) {
|
|
logger.error(`Subtask activity API error: ${err}`);
|
|
res.status(500).json({ error: 'Failed to fetch subtask activity' });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|