/**
* POST /ds-devices/{serial_number}/logs/exports — ログのエクスポートの準備(指示書AY 段階7-1。D14・D-179)
*
* 仕様の正は `d-watch-api.yaml` の /ds-devices/{serial_number}/logs/exports。
* 制御盤に zip を作らせ、prepared が届くまで最大50秒待ってから 201(受け取りは `download_url`)。
* ★サーバーには保存しない★。★POST が切れたら(`request.signal`)エクスポートを取り消す(4-9)★。
*/
import { NextResponse } from "next/server";
import { parsePathSerialNumber } from "@/lib/api/path-params";
import { db } from "@/lib/db";
import { USE_FIXTURES } from "@/lib/db/use-fixtures";
import { errorBody, jsonError } from "@/lib/ds-agent/agent-request";
import { ensureFixtureAgent } from "@/lib/ds-agent/fixture-agent";
import { getRelay } from "@/lib/ds-agent/relay";
import { ensureDsAgentLogging } from "@/lib/ds-agent/relay-log";
import { parseExportRequest } from "@/lib/ds-logs/validate";
import { logEvent, withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { DsLogExportResponse } from "@/lib/types";
const ROUTE = "/api/ds-devices/[serial_number]/logs/exports";
export async function POST(
request: Request,
{ params }: { params: Promise<{ serial_number: string }> }
) {
return withRequestId(request, async () => {
ensureDsAgentLogging();
const { serial_number: serialRaw } = await params;
const parsedSerial = parsePathSerialNumber(serialRaw);
if (!parsedSerial.ok) return NextResponse.json(parsedSerial.error, { status: parsedSerial.status });
const serial = parsedSerial.query;
// ★JSON でない・オブジェクトでない本体は 422「JSONの形式が不正です」(note と同じ。V8・V9)★
const parsedBody = parseExportRequest(await request.text());
if (!parsedBody.ok) return NextResponse.json(parsedBody.error, { status: parsedBody.status });
let exists: boolean;
try {
exists = await db.dsDeviceExists(serial);
} catch (e) {
const body = errorBody("internal_error", "データの取得に失敗しました");
logApiError({ route: ROUTE, method: "POST", body, status: 500, err: e });
return NextResponse.json(body, { status: 500 });
}
if (!exists) return jsonError(404, "not_found", "デバイスが見つかりません");
const relay = getRelay();
if (USE_FIXTURES) ensureFixtureAgent(relay, serial);
const started = Date.now();
const result = await relay.createExport(serial, parsedBody.query.fileIds, request.signal);
switch (result.kind) {
case "offline": {
const lastSeen = relay.agentStatus(serial).last_seen_at;
return jsonError(409, "agent_offline", "制御盤が接続されていません", {
last_seen_at: lastSeen ? [lastSeen] : [],
});
}
case "busy":
return result.code === "export_busy"
? jsonError(429, "export_busy", "この制御盤は別のエクスポートを処理中です")
: jsonError(429, "agent_busy", "制御盤が混み合っています");
case "timeout":
return jsonError(504, "agent_timeout", "制御盤から応答がありません");
case "agent_error":
return jsonError(502, "agent_error", "制御盤でエラーが発生しました", { agent_code: [result.code] });
case "prepared": {
logEvent("info", {
event: "ds_logs.export_prepared",
serial_number: serial,
file_count: result.fileCount,
total_bytes: result.totalBytes,
duration_ms: Date.now() - started,
});
const body: DsLogExportResponse = {
export_id: result.exportId,
download_url: `/api/ds-devices/${encodeURIComponent(serial)}/logs/exports/${result.exportId}`,
total_bytes: result.totalBytes,
file_count: result.fileCount,
expires_in_sec: result.expiresInSec,
};
return NextResponse.json(body, { status: 201 });
}
}
});
}