{"record":{"id":"6f9ee81bc968a7df","repo":"nexu-io/open-design","slug":"no-slides-to-export","errorCode":null,"errorMessage":"no slides to export","messagePattern":"no slides to export","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/deck-export.ts","lineNumber":144,"sourceCode":"\n// Standard PowerPoint 16:9 slide is 13.333\" x 7.5\". We keep 13.333\" as the slide\n// width and derive the height from the deck's actual aspect ratio, so a 4:3,\n// square, or portrait deck gets a correctly-proportioned slide instead of being\n// letterboxed into a 16:9 frame.\nconst PPTX_SLIDE_WIDTH_IN = 13.333;\n\n/**\n * Assembles per-slide images into a screenshot-based .pptx — one full-bleed\n * image per slide. The slide aspect ratio follows the deck's authored size\n * (`opts.aspect` = width/height); falls back to 16:9. The slides are\n * pixel-perfect images (not editable text), the \"exactly what you see\" export\n * mode. Returns the .pptx bytes.\n */\nexport async function buildScreenshotPptx(\n  images: SlideImage[],\n  opts: { title?: string; aspect?: number } = {},\n): Promise<Buffer> {\n  if (images.length === 0) throw new Error('no slides to export');\n  const pptx = new PptxGenJS();\n  const aspect = opts.aspect && Number.isFinite(opts.aspect) && opts.aspect > 0 ? opts.aspect : 16 / 9;\n  if (Math.abs(aspect - 16 / 9) < 0.01) {\n    pptx.layout = 'LAYOUT_16x9';\n  } else {\n    const height = Number((PPTX_SLIDE_WIDTH_IN / aspect).toFixed(3));\n    pptx.defineLayout({ name: 'OD_DECK', width: PPTX_SLIDE_WIDTH_IN, height });\n    pptx.layout = 'OD_DECK';\n  }\n  pptx.author = 'Open Design';\n  if (opts.title) pptx.title = opts.title;\n  pptx.subject = 'Screenshot-based PPTX';\n  for (const img of images) {\n    const slide = pptx.addSlide();\n    slide.addImage({\n      data: `data:image/${img.jpeg ? 'jpeg' : 'png'};base64,${img.buffer.toString('base64')}`,\n      x: 0,\n      y: 0,","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/deck-export.ts#L126-L162","documentation":"Thrown by buildScreenshotPptx in apps/daemon/src/deck-export.ts:144 when its `images` argument is an empty array. The screenshot-based .pptx builder needs at least one full-bleed slide image to lay out one slide per page, so zero images is treated as an unrecoverable export request rather than producing an empty deck file. It is a plain `Error` (no HTTP status), so the route layer in import-export-routes.ts is responsible for translating it into an HTTP response. The `images` come from `SlideImage[]` (each `{ buffer, jpeg }`) produced by `decodeSlideDataUrls` or `readSlideFiles` from the desktop renderer output.","triggerScenarios":"Calling `buildScreenshotPptx(images, opts)` where `images.length === 0`. In the live route (import-export-routes.ts:997) this happens when `decodeSlideDataUrls(rendered.slides)` or `readSlideFiles(safeFiles)` yields zero entries — i.e. the desktop renderer returned no `slides`, or the rendered slide file list was empty.","commonSituations":"Exporting a deck artifact that has zero slides; the renderer failed or timed out and returned an empty slides array; a page-mode export was misrouted into the deck export path; the outputDir contained no image files and readSlideFiles mapped over an empty list.","solutions":["Confirm the artifact being exported is a deck/page with at least one renderable slide before issuing the export request.","If calling buildScreenshotPptx directly, guard with `if (images.length === 0) throw new Error('deck has no slides')` (or return early) before the call.","Inspect the renderer response: check that `rendered.slides` is a non-empty array of data URLs, or that `safeFiles` resolves to existing image files on disk.","In the route handler, catch this Error and translate it into a 400/422 with a user-facing message instead of letting it surface as a 500."],"exampleFix":"// before\nbuffer = await buildScreenshotPptx(images, { title: resolvedTitle, ...(aspect ? { aspect } : {}) });\n\n// after\nif (images.length === 0) {\n  return sendApiError(res, 400, 'Deck has no slides to export.');\n}\nbuffer = await buildScreenshotPptx(images, { title: resolvedTitle, ...(aspect ? { aspect } : {}) });","handlingStrategy":"validation","validationCode":"function hasSlides(images: unknown): images is SlideImage[] {\n  return Array.isArray(images) && images.length > 0;\n}\n// before calling buildScreenshotPptx:\nif (!hasSlides(images)) {\n  return sendApiError(res, 400, 'Deck has no slides to export.');\n}","typeGuard":"function isSlideImage(v: unknown): v is SlideImage {\n  return typeof v === 'object' && v !== null\n    && Buffer.isBuffer((v as { buffer?: unknown }).buffer)\n    && typeof (v as { jpeg?: unknown }).jpeg === 'boolean';\n}\nfunction hasRenderedSlides(v: unknown): v is SlideImage[] {\n  return Array.isArray(v) && v.length > 0 && v.every(isSlideImage);\n}","tryCatchPattern":"try {\n  buffer = await buildScreenshotPptx(images, opts);\n} catch (err) {\n  if (err instanceof Error && err.message === 'no slides to export') {\n    return sendApiError(res, 400, 'Deck has no slides to export.');\n  }\n  throw err;\n}","preventionTips":["Validate that the renderer returned at least one slide image before entering the export branch.","Treat an empty rendered.slides array as a renderer failure and surface it earlier, not as an export-time crash.","Add a route-level guard before buildScreenshotPptx/buildScreenshotPdf so the generic Error never reaches the client as a 500."],"tags":["deck-export","validation","renderer","pptx"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}