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
19 changes: 19 additions & 0 deletions apps/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from 'express'
import type { NextFunction, Request, Response } from 'express'
import cors from 'cors'
import projectRouter from './modules/project';
import runRouter from './modules/run';
Expand All @@ -12,6 +13,7 @@ import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { runQueue } from './modules/worker';
import { logger } from './modules/utils';

const app = express();
app.use(cors())
Expand All @@ -33,6 +35,23 @@ createBullBoard({
});
app.use("/admin/queues", bullBoardAdapter.getRouter());

// Express 5 forwards rejected async handlers here; without this they'd be
// answered by the default handler, which logs nothing and can leak stacks.
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
logger.error(`Unhandled error on ${req.method} ${req.originalUrl}: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
if (res.headersSent) {
return next(err);
}
return res.status(500).json({ success: false, message: "Internal server error" });
});

process.on("unhandledRejection", (reason) => {
logger.error(`Unhandled promise rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
});
process.on("uncaughtException", (err) => {
logger.error(`Uncaught exception: ${err.stack ?? err.message}`);
});

console.table(expressListEndpoints(questionRouter));
app.listen(3000, () =>{
console.log("Server is running on port 3000")
Expand Down
32 changes: 29 additions & 3 deletions apps/backend/src/modules/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ GET /chat/:projectId/history → all past runs' events, for reload
chatRouter.post('/', auth, createRun)
chatRouter.post('/:projectId', auth, createRun)

// Event rows are written by the agent worker; a corrupt/truncated one must not
// take down the whole state response.
function parseEventContent(content: string | null, runId: string): unknown {
if(!content) return null
try{
return JSON.parse(content)
} catch(e){
logger.error(`Failed to parse stored event content for run ${runId}: ${e}`)
return null
}
}

async function markRunFailed(runId: string){
try{
await prisma.run.update({where: {id: runId}, data: {status: 'FAILED', endedAt: new Date()}})
} catch(e){
logger.error(`Failed to mark run ${runId} as FAILED: ${e}`)
}
}

async function createRun(req: Request, res: Response){
const userId = req.headers.userid
let projectId = req.params?.projectId
Expand Down Expand Up @@ -78,6 +98,9 @@ async function createRun(req: Request, res: Response){
logger.info(`Added to run queue`)
} catch(e){
logger.error(`Failed to enqueue run ${run.id}: ${e}`)
// The Run row already exists — leaving it IN_PROGRESS would have the
// frontend waiting on a stream for a job that was never queued.
await markRunFailed(run.id)
return res.status(500).json({success: false, message: `Failed to start run`})
}

Expand Down Expand Up @@ -108,7 +131,7 @@ chatRouter.get('/:runId/state', auth, async (req: Request, res: Response) => {
where: { runId, type: run.status === 'CLARIFICATION_NEEDED' ? 'clarification_needed' : 'select_design' },
orderBy: { createdAt: 'desc' },
})
pauseEvent = event?.content ? JSON.parse(event.content) : null
pauseEvent = parseEventContent(event?.content ?? null, runId)
}

let completedEvent: unknown = null
Expand All @@ -117,7 +140,7 @@ chatRouter.get('/:runId/state', auth, async (req: Request, res: Response) => {
where: { runId, type: 'run_completed' },
orderBy: { createdAt: 'desc' },
})
completedEvent = event?.content ? JSON.parse(event.content) : null
completedEvent = parseEventContent(event?.content ?? null, runId)
}

let failedEvent: unknown = null
Expand All @@ -126,7 +149,7 @@ chatRouter.get('/:runId/state', auth, async (req: Request, res: Response) => {
where: { runId, type: 'run_failed' },
orderBy: { createdAt: 'desc' },
})
failedEvent = event?.content ? JSON.parse(event.content) : null
failedEvent = parseEventContent(event?.content ?? null, runId)
}

return res.status(200).json({
Expand Down Expand Up @@ -185,6 +208,9 @@ chatRouter.post('/:projectId/:runId/continue', auth, async (req: Request, res: R
logger.info(`Continuing run ${run.id}`)
} catch(e){
logger.error(`Failed to re-enqueue run ${run.id}: ${e}`)
// Status was flipped to IN_PROGRESS just above, so it has to be undone
// or the run is stuck awaiting a job that doesn't exist.
await markRunFailed(run.id)
return res.status(500).json({success: false, message: `Failed to continue run`})
}

Expand Down
13 changes: 11 additions & 2 deletions apps/backend/src/modules/design.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ designRouter.post("/:projectId", internalAuth, async( req: Request, res: Respons
message: "Invalid projectId",
});
}
if (!Array.isArray(designs) || designs.some((d) => typeof d !== "string")) {
return res.status(400).json({
success: false,
message: "designs must be an array of html strings",
});
}
let result
try{
console.log(`Saving designs to the db`)
logger.info(`Saving designs to the db`)
result = await Promise.all(designs.map((design: string) =>
prisma.design.create({
data:{
Expand Down Expand Up @@ -66,12 +72,13 @@ designRouter.get("/:projectId/getDesigns", auth, async (req: Request, res: Respo
projectId: projectId,
},
});
logger.info(`Designs are: ${designs}`)
logger.info(`Found ${designs.length} design(s) for project ${projectId}`)
return res.status(200).json({
success: true,
data: designs,
});
} catch (e) {
logger.error(`Failed to list designs for project ${projectId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down Expand Up @@ -110,6 +117,7 @@ designRouter.get("/:projectId/selectedDesign", auth, async (req: Request, res: R
data: design,
});
} catch (e) {
logger.error(`Failed to fetch selected design for project ${projectId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down Expand Up @@ -170,6 +178,7 @@ designRouter.patch("/:projectId/designs/:designId", auth, async (req: Request, r
data: selectedDesign,
});
} catch (e) {
logger.error(`Failed to select design ${designId} for project ${projectId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down
64 changes: 36 additions & 28 deletions apps/backend/src/modules/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ const projectRouter = Router();
const r2 = new R2();

projectRouter.get("/", auth, async (req: AuthRequest, res: Response) => {
const userId = req.user.id
const userId = req.user?.id
if(!userId){

res.status(401).json({success: false, message: `UserId not given`})
return res.status(401).json({success: false, message: `UserId not given`})
}
await prisma.project.findMany({where: {userId: userId}})
const projects = await prisma.project.findMany({where: {userId: userId}})

if(!projects){
res.status(404).json({success: false, message: `Projects not found`})
try{
const projects = await prisma.project.findMany({where: {userId: userId}})
return res.status(200).json({success: true, data: projects})
}
catch(e){
logger.error(`Failed to list projects for user ${userId}: ${e}`)
return res.status(500).json({success: false, message: `Internal server error`})
}
res.status(200).json({success: true, data: projects})
});

projectRouter.post("/", async (req: Request, res: Response) => {
Expand All @@ -53,24 +54,30 @@ projectRouter.post("/", async (req: Request, res: Response) => {
}
return res.status(201).json({success: true, message: `project created`, data: saveIntoDB})
}catch(e){
return res.json(500).json({message: `Internal server error`})
logger.error(`Failed to create project for user ${userId}: ${e}`)
return res.status(500).json({success: false, message: `Internal server error`})
}
})
projectRouter.get("/:projectId", auth, async (req: Request, res: Response) => {
const projectId = req.params.projectId
if(!projectId){

res.status(401).json({success: false, message: `UserId not given`})
}
if (typeof projectId !== "string") {
if (typeof projectId !== "string" || !projectId) {
return res.status(400).json({
success: false,
message: "Invalid projectId",
});
}
const projects = await prisma.project.findUniqueOrThrow({where: {id: projectId}})

res.status(200).json({success: true, data: projects})
try{
const project = await prisma.project.findUnique({where: {id: projectId}})
if(!project){
return res.status(404).json({success: false, message: `Project not found`})
}
return res.status(200).json({success: true, data: project})
}
catch(e){
logger.error(`Failed to fetch project ${projectId}: ${e}`)
return res.status(500).json({success: false, message: `Internal server error`})
}
});
projectRouter.patch('/:projectId', auth, async (req: Request, res: Response) =>{
const projectId = req.params.projectId
Expand Down Expand Up @@ -106,36 +113,37 @@ projectRouter.patch('/:projectId', auth, async (req: Request, res: Response) =>{
});
}
try{
const project = await prisma.project.findUniqueOrThrow({where: {id: projectId}})
const project = await prisma.project.findUnique({where: {id: projectId}})

if(!project){
return res.status(404).json({message: `Project not found`})
return res.status(404).json({success: false, message: `Project not found`})
}
const dbUpdate = await prisma.project.update({where: {id: projectId}, data: data})
return res.status(200).json({success: true, data: dbUpdate})
}
catch(e){
return res.status(500).json({message: `Internal Server error`})
logger.error(`Failed to update project ${projectId}: ${e}`)
return res.status(500).json({success: false, message: `Internal Server error`})
}
})

projectRouter.delete('/:projectId', auth, async (req: Request, res: Response) =>{
const projectId = req.params.projectId
if(!projectId){

res.status(401).json({success: false, message: `UserId not given`})
}
if (typeof projectId !== "string") {
if (typeof projectId !== "string" || !projectId) {
return res.status(400).json({
success: false,
message: "Invalid projectId",
});
}
const dbUpdate = await prisma.project.delete({where: {id: projectId}})
if(!dbUpdate){
return res.send(500).json({success: false, message: `Failed to update DB`})

try{
await prisma.project.delete({where: {id: projectId}})
return res.status(200).json({success: true})
}
catch(e){
logger.error(`Failed to delete project ${projectId}: ${e}`)
return res.status(500).json({success: false, message: `Failed to delete project`})
}
res.status(200).json({success: true})
});

// Files are synced sandbox -> R2 by E2BSandbox.SyncR2() during the run, so this
Expand Down
47 changes: 29 additions & 18 deletions apps/backend/src/modules/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ questionRouter.get('/:projectId/getQuestions', auth, async (req: Request, res: R
return res.status(200).json({success: true, data: questions})
}
catch(e){
logger.error(`Failed to fetch questions for project ${projectId}: ${e}`)
return res.status(500).json({
success: false,
message: "Internal server error",
Expand All @@ -42,24 +43,34 @@ questionRouter.post('/:projectId/:runId', internalAuth, async (req: Request, res
if(typeof projectId !== 'string' || typeof runId !== 'string'){
return res.status(400).json({success: false, message: `Bad types of the params`})
}
const result = await Promise.all(
questionsObj.map((question: IncomingQuestion) =>
prisma.question.create({
data: {
id: randomUUIDv7(),
runId,
projectId,
question: question.question,
options: question.option,
createdAt: new Date(),
},
})
)
);
return res.status(201).json({
success: true,
data: result
});
if(!Array.isArray(questionsObj) || questionsObj.length === 0){
return res.status(400).json({success: false, message: `questionsObj must be a non-empty array`})
}

try{
const result = await Promise.all(
questionsObj.map((question: IncomingQuestion) =>
prisma.question.create({
data: {
id: randomUUIDv7(),
runId,
projectId,
question: question.question,
options: question.option,
createdAt: new Date(),
},
})
)
);
return res.status(201).json({
success: true,
data: result
});
}
catch(e){
logger.error(`Failed to save questions for run ${runId}: ${e}`)
return res.status(500).json({success: false, message: `Internal server error`})
}
})

questionRouter.post('/:projectId/:runId/answers', auth, async (req: Request, res: Response) =>{
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/modules/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ runRouter.get("/:projectId/runs", auth, async (req: Request, res: Response) => {
data: runs,
});
} catch (e) {
logger.error(`Failed to list runs for project ${projectId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down Expand Up @@ -90,6 +91,7 @@ runRouter.get("/:projectId/runs/:runId", auth, async (req: Request, res: Respons
data: run,
});
} catch (e) {
logger.error(`Failed to fetch run ${runId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down Expand Up @@ -142,6 +144,7 @@ runRouter.get("/:projectId/:runId/todos", auth, async (req: Request, res: Respon
data: todos,
});
} catch (e) {
logger.error(`Failed to fetch todos for run ${runId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down Expand Up @@ -204,6 +207,7 @@ runRouter.get("/:projectId/:runId/summaries", auth, async (req: Request, res: Re
data: summaries,
});
} catch (e) {
logger.error(`Failed to fetch summaries for run ${runId}: ${e}`);
return res.status(500).json({
success: false,
message: "Internal server error",
Expand Down
Loading