From 34f211538a0f9fa097b9174b6d7b39cadd38330c Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 9 Oct 2026 05:50:21 +0200 Subject: [PATCH] fix(preview): #502 landing fixes Skip zero-size spreadsheet cells in renderTile. On sheets past the 8,000,000 px scroll cap, a cell clipped to nothing at the spacer's edge (or a visible row or column the worker clamped to 0 px) was still created, and the cell padding and border drew it as a 5 px box below the spacer that grew the scroll area. The size is now computed before the element is created and such cells are skipped, the same way the heading loops already skip 0 px rows and columns. spreadsheet-preview.js is not an input of SPREADSHEET_ASSET_VERSION, so the asset token stays valid. Add zh-CN entries for the static spreadsheet preview strings (loading, too large, no visible worksheets, empty worksheet, the warnings label, timeout, failure, the four parser start and message failures, and the unavailable message from panels-ui). The file-preview body is not a skipped surface, so the exact-match entries apply with no code change. The worker's admission refusal messages and the dynamic status message stay English for a follow-up. docs/security-architecture.md described the attachment gate as a 6-extension allowlist; it now names SUPPORTED_ATTACHMENT_EXTENSIONS in src/attachment-registry.ts and what it covers, including the xlsx this PR adds. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/security-architecture.md | 5 +++-- src/web/public/i18n.js | 12 ++++++++++++ src/web/public/spreadsheet-preview.js | 12 +++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/security-architecture.md b/docs/security-architecture.md index d1153fbf..072cd369 100644 --- a/docs/security-architecture.md +++ b/docs/security-architecture.md @@ -354,8 +354,9 @@ is by id (`GET /api/sessions/:id/attachments/:attachmentId/raw`, same download c the `/root` and `/etc` trees, extendable via `attachmentBlockedPaths` / `CODEMAN_ATTACHMENT_BLOCKED_PATHS`) on every request. Unlike the workspace file routes, attachments are intentionally **cross‑workspace** — so the effective gate -is the blocklist + a 6‑extension allowlist (`png/pdf/docx/pptx/md/txt`), not -realpath containment. +is the blocklist + an extension allowlist (`SUPPORTED_ATTACHMENT_EXTENSIONS` in +`src/attachment-registry.ts`: images, pdf/docx/pptx/xlsx, audio/video, md/txt and +other text), not realpath containment. Two registration paths, with **different trust**: diff --git a/src/web/public/i18n.js b/src/web/public/i18n.js index 9f791263..ff810617 100644 --- a/src/web/public/i18n.js +++ b/src/web/public/i18n.js @@ -858,6 +858,18 @@ 'Wrap lines': '自动换行', 'Unsaved changes': '未保存的更改', Saved: '已保存', + 'Loading spreadsheet…': '正在加载电子表格…', + 'This workbook is too large to preview (10 MB limit).': '此工作簿太大,无法预览(上限 10 MB)。', + 'This workbook has no visible worksheets.': '此工作簿没有可见的工作表。', + 'This worksheet is empty.': '此工作表为空。', + 'Some workbook features are not shown': '部分工作簿功能未显示', + 'Spreadsheet preview timed out.': '电子表格预览超时。', + 'Spreadsheet preview failed': '电子表格预览失败', + 'Spreadsheet parser failed.': '电子表格解析器出错。', + 'Spreadsheet parser failed to start': '电子表格解析器启动失败', + 'Spreadsheet parser message failed.': '电子表格解析器消息出错。', + 'Spreadsheet parser message failed': '电子表格解析器消息出错', + 'Spreadsheet preview is unavailable.': '电子表格预览不可用。', 'Export as JSON': '导出为 JSON', 'Export as Markdown': '导出为 Markdown', 'Mark all read': '全部标为已读', diff --git a/src/web/public/spreadsheet-preview.js b/src/web/public/spreadsheet-preview.js index 2930c121..d43334a1 100644 --- a/src/web/public/spreadsheet-preview.js +++ b/src/web/public/spreadsheet-preview.js @@ -251,6 +251,13 @@ }); } for (const cell of tile.cells.slice(0, 2500)) { + const merge = mergeByAnchor.get(`${cell.row}:${cell.col}`); + const height = rowSpan(cell.row, merge?.r2 || cell.row); + const width = colSpan(cell.col, merge?.c2 || cell.col); + // A cell clipped to nothing at the spacer's edge (or sized 0 px) is + // skipped like its heading: padding and border would still draw it as a + // small box below the spacer and grow the scroll area. + if (height <= 0 || width <= 0) continue; const element = document.createElement('div'); element.className = `spreadsheet-cell spreadsheet-style-${Number(cell.styleId) || 0}`; element.dataset.row = String(cell.row); @@ -258,9 +265,8 @@ element.textContent = String(cell.text ?? ''); element.style.top = `${rowTop(cell.row)}px`; element.style.left = `${colLeft(cell.col)}px`; - const merge = mergeByAnchor.get(`${cell.row}:${cell.col}`); - element.style.height = `${rowSpan(cell.row, merge?.r2 || cell.row)}px`; - element.style.width = `${colSpan(cell.col, merge?.c2 || cell.col)}px`; + element.style.height = `${height}px`; + element.style.width = `${width}px`; cellsLayer.appendChild(element); } // Headings take their size from the same axis math as the cells, so custom