| 469 | const BOARD_RE = /^\/boards\/([A-Za-z0-9_-]+)(\/.*)?$/; |
| 470 | |
| 471 | export async function fetchHandler(req: Request): Promise<Response> { |
| 472 | const url = new URL(req.url); |
| 473 | const origin = url.origin; |
| 474 | |
| 475 | if (req.method === "GET" && url.pathname === "/health") return handleHealth(); |
| 476 | if (req.method === "GET" && url.pathname === "/") return handleIndex(); |
| 477 | if (req.method === "POST" && url.pathname === "/api/boards") return handlePublish(req, origin); |
| 478 | |
| 479 | if (req.method === "POST" && url.pathname === "/shutdown") { |
| 480 | if (hasActiveBoards()) { |
| 481 | return Response.json( |
| 482 | { |
| 483 | error: "Refusing /shutdown: daemon has active boards. Submit or close them first.", |
| 484 | activeBoards: nonDoneCount(), |
| 485 | }, |
| 486 | { status: 409 }, |
| 487 | ); |
| 488 | } |
| 489 | setTimeout(() => gracefulShutdown(0), 50); |
| 490 | return Response.json({ shuttingDown: true }); |
| 491 | } |
| 492 | |
| 493 | const m = url.pathname.match(BOARD_RE); |
| 494 | if (m) { |
| 495 | const id = m[1]!; |
| 496 | const subpath = m[2] || ""; |
| 497 | const board = boards.get(id); |
| 498 | if (!board) { |
| 499 | return new Response(boardExpiredHtml(id), { |
| 500 | status: 404, |
| 501 | headers: { "Content-Type": "text/html; charset=utf-8" }, |
| 502 | }); |
| 503 | } |
| 504 | // Bare /boards/<id> → 301 to /boards/<id>/ so relative URLs in board JS |
| 505 | // resolve against the right base (./api/feedback → /boards/<id>/api/feedback). |
| 506 | if (req.method === "GET" && subpath === "") { |
| 507 | return new Response(null, { |
| 508 | status: 301, |
| 509 | headers: { Location: `/boards/${id}/` }, |
| 510 | }); |
| 511 | } |
| 512 | if (req.method === "GET" && subpath === "/") return handleBoardGet(board); |
| 513 | if (req.method === "GET" && subpath === "/api/progress") return handleBoardProgress(board); |
| 514 | if (req.method === "POST" && subpath === "/api/feedback") { |
| 515 | return withBoardMutex(id, () => handleBoardFeedback(board, req)); |
| 516 | } |
| 517 | if (req.method === "POST" && subpath === "/api/reload") { |
| 518 | return withBoardMutex(id, () => handleBoardReload(board, req)); |
| 519 | } |
| 520 | } |
| 521 | |
| 522 | return new Response("Not found", { status: 404 }); |
| 523 | } |
| 524 | |
| 525 | // ─── Startup ───────────────────────────────────────────────────── |
| 526 | |