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 => { 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; }