feat(logs): add scenario run logs export

- Backend: logs export endpoint for CSV and Markdown formats
- Frontend: ExportLogsModal with download button on run detail page
- Updated i18n and CHANGELOG
- Version bump: 1.9.1 -> 1.10.0
This commit is contained in:
Andrii Arsenin
2026-09-15 13:04:59 +03:00
parent f1aac987db
commit ce85aa02f1
10 changed files with 230 additions and 7 deletions
+45
View File
@@ -407,6 +407,51 @@ export class ScenarioService {
return Object.assign(run, { logs });
}
async exportRunLogs(
scenarioId: string,
runId: string,
format: "csv" | "md",
): Promise<string> {
await this.findOne(scenarioId); // 404 guard
const run = await this.runRepo.findOne({
where: { id: runId, scenarioId },
});
if (!run)
throw new NotFoundException(
`Run ${runId} not found in scenario ${scenarioId}`,
);
const logs = await this.runLogRepo.find({
where: { runId },
order: { createdAt: "ASC" },
});
if (format === "csv") {
const escapeCsv = (value: string): string =>
/[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
const rows = [
["Timestamp", "Level", "Message"].map(escapeCsv).join(","),
...logs.map((l) =>
[l.createdAt.toISOString(), l.level, l.message]
.map((v) => escapeCsv(String(v)))
.join(","),
),
];
return rows.join("\n");
}
const lines = [
`# Run ${runId} logs`,
"",
"| Timestamp | Level | Message |",
"| --- | --- | --- |",
...logs.map(
(l) =>
`| ${l.createdAt.toISOString()} | ${l.level} | ${l.message.replace(/\|/g, "\\|").replace(/\n/g, "<br>")} |`,
),
];
return lines.join("\n");
}
async waitForRun(
scenarioId: string,
runId: string,