Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions TASK_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,22 @@ All REST API endpoints in this project return JSON with the following top-level

- `ok` (boolean): `true` for success, `false` for errors
- `error` (string): short human-readable summary
- `code` (string): optional machine-readable error code for programmatic handling
- `details` (string[]): optional list of validation-specific messages
- `limits` (object): optional object describing relevant request limits

This format is currently implemented for:
- `POST /api/task-submissions/validate`
This format is implemented for all REST API endpoints. Common error codes include:

- `INVALID_FORM_DATA` - Request body is not valid multipart form data
- `INVALID_JSON` - Request body is not valid JSON
- `INVALID_PAYLOAD` - Request payload is invalid
- `FILE_VALIDATION_FAILED` - File validation failed
- `TASK_CREATION_FAILED` - Task creation failed
- `TASK_NOT_FOUND` - Task not found
- `SUBMISSION_FAILED` - Work submission failed
- `STATS_FETCH_FAILED` - Failed to retrieve dashboard statistics
- `HEALTH_CHECK_FAILED` - Failed to retrieve health status
- `RATE_LIMIT_EXCEEDED` - Rate limit exceeded

---

Expand Down
33 changes: 22 additions & 11 deletions frontend/src/app/api/dashboard/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getDashboardStatistics } from "@/lib/dashboard-stats";
import { buildNoStoreJson } from "@/lib/api-response";
import { buildErrorResponse, buildNoStoreJson } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";

export const runtime = "nodejs";
Expand All @@ -16,15 +16,26 @@ export async function GET(request: Request) {
return rateLimitResponse;
}

const result = getDashboardStatistics();
try {
const result = getDashboardStatistics();

return buildNoStoreJson(
{
ok: true,
stats: result.stats,
meta: result.meta,
},
200,
rateLimitHeaders,
);
return buildNoStoreJson(
{
ok: true,
stats: result.stats,
meta: result.meta,
},
200,
rateLimitHeaders,
);
} catch (error) {
return buildErrorResponse(
"Failed to retrieve dashboard statistics.",
500,
"STATS_FETCH_FAILED",
error instanceof Error ? [error.message] : undefined,
undefined,
rateLimitHeaders,
);
}
}
20 changes: 16 additions & 4 deletions frontend/src/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";

import { buildHealthReport } from "@/lib/health";
import { buildErrorResponse } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";

// Health must always reflect the live process, never a cached/static response.
Expand All @@ -13,8 +14,19 @@ export async function GET(request: Request) {
return rateLimitResponse;
}

return NextResponse.json(buildHealthReport(), {
status: 200,
headers: { "Cache-Control": "no-store", ...rateLimitHeaders },
});
try {
return NextResponse.json(buildHealthReport(), {
status: 200,
headers: { "Cache-Control": "no-store", ...rateLimitHeaders },
});
} catch (error) {
return buildErrorResponse(
"Failed to retrieve health status.",
500,
"HEALTH_CHECK_FAILED",
error instanceof Error ? [error.message] : undefined,
undefined,
rateLimitHeaders,
);
}
}
29 changes: 14 additions & 15 deletions frontend/src/app/api/task-submissions/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
validateTaskSubmissionFiles,
} from "@/lib/task-submission-files";
import { buildErrorResponse } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";

export const runtime = "nodejs";
Expand Down Expand Up @@ -35,12 +36,12 @@ export async function POST(request: Request) {
try {
formData = await request.formData();
} catch {
return buildNoStoreJson(
{
ok: false,
error: "Please upload files using a valid form.",
},
return buildErrorResponse(
"Please upload files using a valid form.",
400,
"INVALID_FORM_DATA",
undefined,
undefined,
rateLimitHeaders,
);
}
Expand All @@ -49,18 +50,16 @@ export async function POST(request: Request) {
const validation = await validateTaskSubmissionFiles(files);

if (!validation.ok) {
return buildNoStoreJson(
return buildErrorResponse(
"Invalid task submission upload.",
validation.status,
"FILE_VALIDATION_FAILED",
validation.errors,
{
ok: false,
error: "Invalid task submission upload.",
details: validation.errors,
limits: {
maxFiles: MAX_TASK_SUBMISSION_FILES,
maxFileSizeBytes: MAX_TASK_SUBMISSION_FILE_SIZE_BYTES,
maxTotalSizeBytes: MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
},
maxFiles: MAX_TASK_SUBMISSION_FILES,
maxFileSizeBytes: MAX_TASK_SUBMISSION_FILE_SIZE_BYTES,
maxTotalSizeBytes: MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
},
validation.status,
rateLimitHeaders,
);
}
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/app/api/tasks/[taskId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getTask } from "@/lib/task-workflow";
import { buildNoStoreJson } from "@/lib/api-response";
import { buildErrorResponse, buildNoStoreJson } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";

export const runtime = "nodejs";
Expand All @@ -20,12 +20,12 @@ export async function GET(request: Request, context: RouteContext) {
const result = getTask(taskId);

if (!result.ok) {
return buildNoStoreJson(
{
ok: false,
error: result.error,
},
return buildErrorResponse(
result.error,
result.status,
"TASK_NOT_FOUND",
undefined,
undefined,
rateLimitHeaders,
);
}
Expand Down
41 changes: 19 additions & 22 deletions frontend/src/app/api/tasks/[taskId]/submissions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
validateTaskSubmissionFiles,
} from "@/lib/task-submission-files";
import { submitTaskWork } from "@/lib/task-workflow";
import { buildNoStoreJson } from "@/lib/api-response";
import { buildErrorResponse, buildNoStoreJson } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";

export const runtime = "nodejs";
Expand All @@ -30,12 +30,12 @@ export async function POST(request: Request, context: RouteContext) {
try {
formData = await request.formData();
} catch {
return buildNoStoreJson(
{
ok: false,
error: "Please submit work using a valid multipart form.",
},
return buildErrorResponse(
"Please submit work using a valid multipart form.",
400,
"INVALID_FORM_DATA",
undefined,
undefined,
rateLimitHeaders,
);
}
Expand All @@ -48,18 +48,16 @@ export async function POST(request: Request, context: RouteContext) {
const validation = await validateTaskSubmissionFiles(files);

if (!validation.ok) {
return buildNoStoreJson(
return buildErrorResponse(
"Invalid task submission upload.",
validation.status,
"FILE_VALIDATION_FAILED",
validation.errors,
{
ok: false,
error: "Invalid task submission upload.",
details: validation.errors,
limits: {
maxFiles: MAX_TASK_SUBMISSION_FILES,
maxFileSizeBytes: MAX_TASK_SUBMISSION_FILE_SIZE_BYTES,
maxTotalSizeBytes: MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
},
maxFiles: MAX_TASK_SUBMISSION_FILES,
maxFileSizeBytes: MAX_TASK_SUBMISSION_FILE_SIZE_BYTES,
maxTotalSizeBytes: MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
},
validation.status,
rateLimitHeaders,
);
}
Expand All @@ -75,13 +73,12 @@ export async function POST(request: Request, context: RouteContext) {
);

if (!result.ok) {
return buildNoStoreJson(
{
ok: false,
error: result.error,
details: result.details,
},
return buildErrorResponse(
result.error,
result.status,
"SUBMISSION_FAILED",
result.details,
undefined,
rateLimitHeaders,
);
}
Expand Down
34 changes: 17 additions & 17 deletions frontend/src/app/api/tasks/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createTask } from "@/lib/task-workflow";
import { buildErrorResponse, buildNoStoreJson } from "@/lib/api-response";
import { createTask, listTasks } from "@/lib/task-workflow";
import { buildNoStoreJson } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
Expand Down Expand Up @@ -71,24 +73,23 @@ export async function POST(request: Request) {
try {
body = await request.json();
} catch {
return buildNoStoreJson(
{
ok: false,
error: "Request body must be valid JSON.",
},
return buildErrorResponse(
"Request body must be valid JSON.",
400,
"INVALID_JSON",
undefined,
undefined,
rateLimitHeaders,
);
}

if (!body || typeof body !== "object") {
return buildNoStoreJson(
{
ok: false,
error: "Invalid task payload.",
details: ["Request body must be a JSON object."],
},
return buildErrorResponse(
"Invalid task payload.",
400,
"INVALID_PAYLOAD",
["Request body must be a JSON object."],
undefined,
rateLimitHeaders,
);
}
Expand All @@ -113,13 +114,12 @@ export async function POST(request: Request) {
});

if (!result.ok) {
return buildNoStoreJson(
{
ok: false,
error: result.error,
details: result.details,
},
return buildErrorResponse(
result.error,
result.status,
"TASK_CREATION_FAILED",
result.details,
undefined,
rateLimitHeaders,
);
}
Expand Down
Loading
Loading