snp-batch-validation/frontend/src/api/batchApi.ts

478 lines
14 KiB
TypeScript

const BASE = import.meta.env.DEV ? '/snp-api/api/batch' : '/snp-api/api/batch';
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`API Error: ${res.status} ${res.statusText}`);
return res.json();
}
async function postJson<T>(url: string, body?: unknown): Promise<T> {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`API Error: ${res.status} ${res.statusText}`);
return res.json();
}
// ── Dashboard ────────────────────────────────────────────────
export interface DashboardStats {
totalSchedules: number;
activeSchedules: number;
inactiveSchedules: number;
totalJobs: number;
}
export interface RunningJob {
jobName: string;
executionId: number;
status: string;
startTime: string;
}
export interface RecentExecution {
executionId: number;
jobName: string;
status: string;
startTime: string;
endTime: string | null;
}
export interface RecentFailure {
executionId: number;
jobName: string;
status: string;
startTime: string;
endTime: string | null;
exitMessage: string | null;
}
export interface FailureStats {
last24h: number;
last7d: number;
}
export interface DashboardResponse {
stats: DashboardStats;
runningJobs: RunningJob[];
recentExecutions: RecentExecution[];
recentFailures: RecentFailure[];
staleExecutionCount: number;
failureStats: FailureStats;
}
// ── Job Execution ────────────────────────────────────────────
export interface JobExecutionDto {
executionId: number;
jobName: string;
status: string;
startTime: string;
endTime: string | null;
exitCode: string | null;
exitMessage: string | null;
}
export interface ApiCallInfo {
apiUrl: string;
method: string;
parameters: Record<string, unknown> | null;
totalCalls: number;
completedCalls: number;
lastCallTime: string;
}
export interface StepExecutionDto {
stepExecutionId: number;
stepName: string;
status: string;
startTime: string;
endTime: string | null;
readCount: number;
writeCount: number;
commitCount: number;
rollbackCount: number;
readSkipCount: number;
processSkipCount: number;
writeSkipCount: number;
filterCount: number;
exitCode: string;
exitMessage: string | null;
duration: number | null;
apiCallInfo: ApiCallInfo | null;
apiLogSummary: StepApiLogSummary | null;
failedRecords?: FailedRecordDto[] | null;
}
export interface ApiLogEntryDto {
logId: number;
requestUri: string;
httpMethod: string;
statusCode: number | null;
responseTimeMs: number | null;
responseCount: number | null;
errorMessage: string | null;
createdAt: string;
}
export interface StepApiLogSummary {
totalCalls: number;
successCount: number;
errorCount: number;
avgResponseMs: number;
maxResponseMs: number;
minResponseMs: number;
totalResponseMs: number;
totalRecordCount: number;
}
export interface ApiLogPageResponse {
content: ApiLogEntryDto[];
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export type ApiLogStatus = 'ALL' | 'SUCCESS' | 'ERROR';
export interface FailedRecordDto {
id: number;
jobName: string;
recordKey: string;
errorMessage: string | null;
retryCount: number;
status: string;
createdAt: string;
}
export interface JobExecutionDetailDto {
executionId: number;
jobName: string;
status: string;
startTime: string;
endTime: string | null;
exitCode: string;
exitMessage: string | null;
jobParameters: Record<string, string>;
jobInstanceId: number;
duration: number | null;
readCount: number;
writeCount: number;
skipCount: number;
filterCount: number;
stepExecutions: StepExecutionDto[];
}
// ── Schedule ─────────────────────────────────────────────────
export interface ScheduleResponse {
id: number;
jobName: string;
cronExpression: string;
description: string | null;
active: boolean;
nextFireTime: string | null;
previousFireTime: string | null;
triggerState: string | null;
createdAt: string;
updatedAt: string;
}
export interface ScheduleRequest {
jobName: string;
cronExpression: string;
description?: string;
active?: boolean;
}
// ── Timeline ─────────────────────────────────────────────────
export interface PeriodInfo {
key: string;
label: string;
}
export interface ExecutionInfo {
executionId: number | null;
status: string;
startTime: string | null;
endTime: string | null;
}
export interface ScheduleTimeline {
jobName: string;
executions: Record<string, ExecutionInfo | null>;
}
export interface TimelineResponse {
periodLabel: string;
periods: PeriodInfo[];
schedules: ScheduleTimeline[];
}
// ── F4: Execution Search ─────────────────────────────────────
export interface ExecutionSearchResponse {
executions: JobExecutionDto[];
totalCount: number;
page: number;
size: number;
totalPages: number;
}
// ── F7: Job Detail ───────────────────────────────────────────
export interface LastExecution {
executionId: number;
status: string;
startTime: string;
endTime: string | null;
}
export interface JobDetailDto {
jobName: string;
lastExecution: LastExecution | null;
scheduleCron: string | null;
}
// ── F8: Statistics ───────────────────────────────────────────
export interface DailyStat {
date: string;
successCount: number;
failedCount: number;
otherCount: number;
avgDurationMs: number;
}
export interface ExecutionStatisticsDto {
dailyStats: DailyStat[];
totalExecutions: number;
totalSuccess: number;
totalFailed: number;
avgDurationMs: number;
}
// ── Recollection History ─────────────────────────────────────
export interface RecollectionHistoryDto {
historyId: number;
apiKey: string;
apiKeyName: string | null;
jobName: string;
jobExecutionId: number | null;
rangeFromDate: string;
rangeToDate: string;
executionStatus: string;
executionStartTime: string | null;
executionEndTime: string | null;
durationMs: number | null;
readCount: number | null;
writeCount: number | null;
skipCount: number | null;
apiCallCount: number | null;
executor: string | null;
recollectionReason: string | null;
failureReason: string | null;
hasOverlap: boolean | null;
createdAt: string;
}
export interface RecollectionSearchResponse {
content: RecollectionHistoryDto[];
totalElements: number;
number: number;
size: number;
totalPages: number;
}
export interface RecollectionStatsResponse {
totalCount: number;
completedCount: number;
failedCount: number;
runningCount: number;
overlapCount: number;
recentHistories: RecollectionHistoryDto[];
}
export interface ApiStatsDto {
callCount: number;
totalMs: number;
avgMs: number;
maxMs: number;
minMs: number;
}
export interface RecollectionDetailResponse {
history: RecollectionHistoryDto;
overlappingHistories: RecollectionHistoryDto[];
apiStats: ApiStatsDto | null;
collectionPeriod: CollectionPeriodDto | null;
stepExecutions: StepExecutionDto[];
}
export interface CollectionPeriodDto {
apiKey: string;
apiKeyName: string | null;
jobName: string | null;
orderSeq: number | null;
rangeFromDate: string | null;
rangeToDate: string | null;
}
// ── API Functions ────────────────────────────────────────────
export const batchApi = {
getDashboard: () =>
fetchJson<DashboardResponse>(`${BASE}/dashboard`),
getJobs: () =>
fetchJson<string[]>(`${BASE}/jobs`),
getJobsDetail: () =>
fetchJson<JobDetailDto[]>(`${BASE}/jobs/detail`),
executeJob: (jobName: string, params?: Record<string, string>) => {
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
return postJson<{ success: boolean; message: string; executionId?: number }>(
`${BASE}/jobs/${jobName}/execute${qs}`);
},
retryFailedRecords: (jobName: string, recordKeys: string[], stepExecutionId: number) => {
const qs = new URLSearchParams({
retryRecordKeys: recordKeys.join(','),
sourceStepExecutionId: String(stepExecutionId),
});
return postJson<{ success: boolean; message: string; executionId?: number }>(
`${BASE}/jobs/${jobName}/execute?${qs.toString()}`);
},
getJobExecutions: (jobName: string) =>
fetchJson<JobExecutionDto[]>(`${BASE}/jobs/${jobName}/executions`),
getRecentExecutions: (limit = 50) =>
fetchJson<JobExecutionDto[]>(`${BASE}/executions/recent?limit=${limit}`),
getExecutionDetail: (id: number) =>
fetchJson<JobExecutionDetailDto>(`${BASE}/executions/${id}/detail`),
stopExecution: (id: number) =>
postJson<{ success: boolean; message: string }>(`${BASE}/executions/${id}/stop`),
// F1: Abandon
getStaleExecutions: (thresholdMinutes = 60) =>
fetchJson<JobExecutionDto[]>(`${BASE}/executions/stale?thresholdMinutes=${thresholdMinutes}`),
abandonExecution: (id: number) =>
postJson<{ success: boolean; message: string }>(`${BASE}/executions/${id}/abandon`),
abandonAllStale: (thresholdMinutes = 60) =>
postJson<{ success: boolean; message: string; abandonedCount?: number }>(
`${BASE}/executions/stale/abandon-all?thresholdMinutes=${thresholdMinutes}`),
// F4: Search
searchExecutions: (params: {
jobNames?: string[];
status?: string;
startDate?: string;
endDate?: string;
page?: number;
size?: number;
}) => {
const qs = new URLSearchParams();
if (params.jobNames && params.jobNames.length > 0) qs.set('jobNames', params.jobNames.join(','));
if (params.status) qs.set('status', params.status);
if (params.startDate) qs.set('startDate', params.startDate);
if (params.endDate) qs.set('endDate', params.endDate);
qs.set('page', String(params.page ?? 0));
qs.set('size', String(params.size ?? 50));
return fetchJson<ExecutionSearchResponse>(`${BASE}/executions/search?${qs.toString()}`);
},
// F8: Statistics
getStatistics: (days = 30) =>
fetchJson<ExecutionStatisticsDto>(`${BASE}/statistics?days=${days}`),
getJobStatistics: (jobName: string, days = 30) =>
fetchJson<ExecutionStatisticsDto>(`${BASE}/statistics/${jobName}?days=${days}`),
// Schedule
getSchedules: () =>
fetchJson<{ schedules: ScheduleResponse[]; count: number }>(`${BASE}/schedules`),
getSchedule: (jobName: string) =>
fetchJson<ScheduleResponse>(`${BASE}/schedules/${jobName}`),
createSchedule: (data: ScheduleRequest) =>
postJson<{ success: boolean; message: string; data?: ScheduleResponse }>(`${BASE}/schedules`, data),
updateSchedule: (jobName: string, data: { cronExpression: string; description?: string }) =>
postJson<{ success: boolean; message: string; data?: ScheduleResponse }>(
`${BASE}/schedules/${jobName}/update`, data),
deleteSchedule: (jobName: string) =>
postJson<{ success: boolean; message: string }>(`${BASE}/schedules/${jobName}/delete`),
toggleSchedule: (jobName: string, active: boolean) =>
postJson<{ success: boolean; message: string; data?: ScheduleResponse }>(
`${BASE}/schedules/${jobName}/toggle`, { active }),
// Timeline
getTimeline: (view: string, date: string) =>
fetchJson<TimelineResponse>(`${BASE}/timeline?view=${view}&date=${date}`),
getPeriodExecutions: (jobName: string, view: string, periodKey: string) =>
fetchJson<JobExecutionDto[]>(
`${BASE}/timeline/period-executions?jobName=${jobName}&view=${view}&periodKey=${periodKey}`),
// Recollection
searchRecollections: (params: {
apiKey?: string;
jobName?: string;
status?: string;
fromDate?: string;
toDate?: string;
page?: number;
size?: number;
}) => {
const qs = new URLSearchParams();
if (params.apiKey) qs.set('apiKey', params.apiKey);
if (params.jobName) qs.set('jobName', params.jobName);
if (params.status) qs.set('status', params.status);
if (params.fromDate) qs.set('fromDate', params.fromDate);
if (params.toDate) qs.set('toDate', params.toDate);
qs.set('page', String(params.page ?? 0));
qs.set('size', String(params.size ?? 20));
return fetchJson<RecollectionSearchResponse>(`${BASE}/recollection-histories?${qs.toString()}`);
},
getStepApiLogs: (stepExecutionId: number, params?: {
page?: number; size?: number; status?: ApiLogStatus;
}) => {
const qs = new URLSearchParams();
qs.set('page', String(params?.page ?? 0));
qs.set('size', String(params?.size ?? 50));
if (params?.status) qs.set('status', params.status);
return fetchJson<ApiLogPageResponse>(
`${BASE}/steps/${stepExecutionId}/api-logs?${qs.toString()}`);
},
getRecollectionDetail: (historyId: number) =>
fetchJson<RecollectionDetailResponse>(`${BASE}/recollection-histories/${historyId}`),
getRecollectionStats: () =>
fetchJson<RecollectionStatsResponse>(`${BASE}/recollection-histories/stats`),
getCollectionPeriods: () =>
fetchJson<CollectionPeriodDto[]>(`${BASE}/collection-periods`),
resetCollectionPeriod: (apiKey: string) =>
postJson<{ success: boolean; message: string }>(`${BASE}/collection-periods/${apiKey}/reset`),
updateCollectionPeriod: (apiKey: string, body: { rangeFromDate: string; rangeToDate: string }) =>
postJson<{ success: boolean; message: string }>(`${BASE}/collection-periods/${apiKey}/update`, body),
};