···55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7788+## [Unreleased]
99+810## [0.29.0] — 2026-04-09
9111012### Added
···296298- Use correct org secret names for keychain unlock
297299- Switch notarization to App Store Connect API key auth
298300299299-## [Unreleased]
300300-301301## [0.14.1] — 2026-03-28
302302303303### Fixed
···312312- Fix E2E test flakiness: replace page reload with addInitScript, add waitForURL before waitForSelector (#305)
313313314314### Changed
315315-- Create PRs for QA audit fix branches (#566)
316316-- Missing test coverage: Drag-fill pattern detection edge cases (#553)
317317-- Missing test coverage: Filter with blank cells, error values, and boolean cells (#538)
318318-- Share dialog: buildShareUrl puts query param after hash fragment (unreachable) (#534)
319319-- Missing test coverage: Recalc after undo/redo and incremental graph consistency (#531)
320320-- Missing test coverage: Pivot table with empty data, null values, and mixed column types (#529)
321321-- Diagrams: hitTestShape ignores rotation — rotated shapes have wrong click target (#528)
322322-- Forms: validateSubmission does not respect conditional visibility (validates hidden required fields) (#520)
323323-- Missing test coverage: CSV export with unicode, multi-byte chars, and formula injection (#511)
324324-- Code quality: API v1 builds SQL via string concatenation instead of prepared statements (#510)
325325-- Bug: API v1 document listing missing 'calendar' in valid types filter (#506)
326326-- Markdown import/export roundtrip loses table alignment and nested list indentation (#505)
327327-- BUG: HOUR/MINUTE/SECOND return NaN for invalid date inputs instead of #VALUE! (#504)
328328-- Bug: Document deletion does not cascade to versions and blobs (#502)
329329-- BUG: VLOOKUP/HLOOKUP approximate match returns wrong result with unsorted data (#501)
330330-- Correctness: Snapshot auto-create inserts with hardcoded type 'doc' for any document (#499)
331331-- Security: WebSocket relay has no message size limit (#498)
332332-- Security: Missing authorization checks on sensitive API endpoints (#496)
333333-- Zero test coverage for server routes, crypto library, DB layer, and validation (#495)
334334-- Bug: DocType type definition missing 'calendar' variant (#494)
335335-- Calendar: comprehensive tests and visual fixes (#482)
336336-- Debug: calendar document creation failing on production (#481)
337337-- Calendar polish: CSS/HTML class alignment, color fixes, tests (#480)
338338-- Decompose docs/main.ts into focused modules (#464)
339339-- Decompose diagrams/main.ts into focused modules (#463)
340340-- Phase 5: extract toolbar, keyboard, cell-editing, grid-rendering from sheets main.ts (#462)
341341-- Phase 4: extract formula-bar, keyboard-shortcuts, clipboard-selection from sheets main.ts (#461)
342342-- Aggressively decompose sheets/main.ts - extract all major UI blocks (#459)
343343-- Decompose sheets/main.ts monolith into focused modules (#458)
344344-- Polish task list checkbox alignment and spacing (#457)
345345-- Bump version to 0.24.0 and update CHANGELOG (#456)
315315+- Refactor duplicated AI chat wiring into shared `initChatWiring()` (#234)
316316+- Decompose monolithic editor entry points into focused modules (#458-464)
346317- Consolidate z-index values into documented CSS custom properties (#450)
347347-- QA batch 22: tests for cross-sheet, custom-format, permissions, named-ranges (#444)
348348-- QA: batch 21 edge case tests for untested modules (#442)
349349-- QA batch 20: continued edge case coverage expansion (#439)
350350-- QA batch 19: theming, automations, range-highlight, export edge cases (#438)
351351-- QA batch 18: formulas & pivot-table edge cases (#437)
352352-- QA batch 17: more untested pure-logic modules (#436)
353353-- No unit tests for server API endpoints or WebSocket relay (#413)
354354-- No unit tests for forms builder, conditional logic, or response pipeline (#412)
355355-- CSS: dark mode colors use oklch() which is not supported in Firefox <113 or Safari <15.4 - no fallback colors defined (#408)
356356-- Fit and finish round 3: provider listener leak, fetch error handling, test gaps (#361)
357357-- Add E2E tests for database views (kanban, gallery, calendar, timeline, pivot) (#302)
358358-- Add E2E tests for forms builder and submission (#301)
359359-- Add E2E tests for diagrams whiteboard (#300)
360360-- Add E2E tests for slides presentations (#299)
361361-- Replace stub tests with real behavioral tests (#298)
362362-- Add unit tests for AI doc actions and blob upload (#297)
363363-- Add unit tests for slides, diagrams, and forms entry points (#296)
364364-- Fix Electron code signing: Developer ID cert + notarization (#264)
365365-- Electron thin client: auto-connect Tailnet backend (#261)
366366-- Wire up Apple notarization for Electron builds (#260)
367367-- Monitor CI for PR #178, merge when green, verify deployment (#255)
368368-- Electron desktop app wrapper (#254)
369369-- Tailscale identity auth - zero-password login via TS headers (#253)
370370-- Monitor and merge PR #176 - CI rename tools Nomad job (#252)
371371-- Refactor duplicated AI chat wiring into shared `initChatWiring()`, removing ~230 lines of duplication (#234)
372372-- Add 11 tests for `initChatWiring` (config propagation, toggle, send/stop/clear, editor-type labels)
318318+- Electron desktop app with Tailnet auto-connect and code signing (#254, #261, #264)
319319+- Tailscale identity auth — zero-password login via TS headers (#253)
373320374321### Security
375375-- Key-sync: encryption keys stored in plaintext in localStorage and sent to server in plaintext - keys should be wrapped with a user-derived key (#405)
376376-- Replace `Math.random()` with `crypto.getRandomValues`/`crypto.randomUUID` in 6 files (#239)
322322+- Replace `Math.random()` with `crypto.getRandomValues`/`crypto.randomUUID` (#239)
377323- Add defense-in-depth XSS escaping for code block language attributes (#238)
378378-- Fix XSS + review findings from AI chat PR #160 (#232)
324324+- Fix XSS findings from AI chat (#232)
379325380326### Accessibility
381327- Add ARIA roles, labels, and live regions to AI chat sidebar (#241)
382382-383383-### Tests
384384-- Add XSS escaping, ARIA attribute, code block rendering, and sheet action edge case tests (#242)
385328386329## [0.12.0] — 2026-03-24
387330
+60
e2e/version-history.spec.ts
···156156 }
157157 });
158158159159+ test('clicking restore button restores the version after confirmation', async ({ page }) => {
160160+ const editor = page.locator('.tiptap');
161161+ await editor.click();
162162+ await page.keyboard.type('Original content');
163163+ await waitForSaved(page);
164164+ await page.waitForTimeout(1000);
165165+166166+ // Overwrite with different content
167167+ await editor.click();
168168+ await page.keyboard.press('Meta+a');
169169+ await page.keyboard.type('Replaced content');
170170+ await waitForSaved(page);
171171+ await page.waitForTimeout(1000);
172172+173173+ // Open version panel and click the first (oldest) version
174174+ await page.click('#btn-history');
175175+ const panel = page.locator('.version-panel');
176176+ await expect(panel).toHaveClass(/open/, { timeout: 5000 });
177177+178178+ const items = panel.locator('.version-panel-item');
179179+ if (await items.count() > 1) {
180180+ // Click the oldest version (last in the list — newest first)
181181+ await items.last().click();
182182+ await expect(panel.locator('.version-panel-preview')).toBeVisible({ timeout: 5000 });
183183+184184+ // Accept the confirm() dialog when restore is clicked
185185+ page.on('dialog', dialog => dialog.accept());
186186+ await page.click('.version-panel-restore');
187187+188188+ // Panel should close after restore
189189+ await expect(panel).not.toHaveClass(/open/, { timeout: 5000 });
190190+ }
191191+ });
192192+193193+ test('clicking restore cancel does not restore', async ({ page }) => {
194194+ const editor = page.locator('.tiptap');
195195+ await editor.click();
196196+ await page.keyboard.type('Keep this content');
197197+ await waitForSaved(page);
198198+ await page.waitForTimeout(1000);
199199+200200+ await page.click('#btn-history');
201201+ const panel = page.locator('.version-panel');
202202+ await expect(panel).toHaveClass(/open/, { timeout: 5000 });
203203+204204+ const items = panel.locator('.version-panel-item');
205205+ if (await items.count() > 0) {
206206+ await items.first().click();
207207+ await expect(panel.locator('.version-panel-preview')).toBeVisible({ timeout: 5000 });
208208+209209+ // Dismiss the confirm() dialog
210210+ page.on('dialog', dialog => dialog.dismiss());
211211+ await page.click('.version-panel-restore');
212212+213213+ // Panel should remain open (restore was cancelled)
214214+ await expect(panel).toHaveClass(/open/);
215215+ await expect(panel.locator('.version-panel-preview')).toBeVisible();
216216+ }
217217+ });
218218+159219 test('name version button is present on version items', async ({ page }) => {
160220 const editor = page.locator('.tiptap');
161221 await editor.click();
+6-3
server/routes/api-v1.ts
···2323// Search documents by name (encrypted names are matched client-side, but
2424// the server can filter by type and return metadata for cross-doc linking)
2525router.get('/api/v1/documents', (req: Request, res: Response) => {
2626- const { type, limit: lim, offset: off } = req.query;
2727- const limit = Math.min(Math.max(parseInt(lim as string) || 50, 1), 200);
2828- const offset = Math.max(parseInt(off as string) || 0, 0);
2626+ const { type: rawType, limit: rawLim, offset: rawOff } = req.query;
2727+ const lim = Array.isArray(rawLim) ? rawLim[0] : rawLim;
2828+ const off = Array.isArray(rawOff) ? rawOff[0] : rawOff;
2929+ const type = Array.isArray(rawType) ? rawType[0] : rawType;
3030+ const limit = Math.min(Math.max(parseInt(lim || '50'), 1), 200);
3131+ const offset = Math.max(parseInt(off || '0'), 0);
29323033 const isTypeFilter = type && VALID_TYPES.includes(type as typeof VALID_TYPES[number]);
3134 const rows = isTypeFilter