Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: trash empty-all button, auto-versioning for data safety, save protection

Trash:
- Added "Empty Trash" button that permanently deletes all trashed
documents with confirmation

Data safety:
- Auto-create version snapshots every 5 minutes (throttled) so data
can be recovered from version history if a bad save occurs
- Set _hadSnapshot=true after any successful meaningful save (not just
on load) so the empty-state protection applies to ALL documents,
not just ones that had a prior snapshot loaded

+49 -4
+6
src/css/app.css
··· 751 751 color: var(--color-text-faint); 752 752 } 753 753 754 + .trash-actions { 755 + padding: 0.5rem 0; 756 + display: flex; 757 + justify-content: flex-end; 758 + } 759 + 754 760 .trash-item { 755 761 opacity: 0.6; 756 762 }
+20 -3
src/landing.ts
··· 529 529 } 530 530 531 531 trashListEl.style.display = ''; 532 - let html = '<div class="doc-list">'; 532 + let html = '<div class="trash-actions"><button class="btn-danger btn-sm trash-empty-all">Empty Trash</button></div>'; 533 + html += '<div class="doc-list">'; 533 534 for (const doc of trashedDocs) { 534 535 const name = doc._decryptedName || 'Encrypted Document'; 535 536 const icon = doc.type === 'doc' ? '&#9998;' : '&#9638;'; ··· 569 570 await fetch(`/api/documents/${id}`, { method: 'DELETE' }); 570 571 trash = removeFromTrash(trash, id); 571 572 localStorage.setItem('tools-trash', JSON.stringify(trash)); 572 - // Remove key 573 573 const k = JSON.parse(localStorage.getItem('tools-keys') || '{}'); 574 574 delete k[id]; 575 575 localStorage.setItem('tools-keys', JSON.stringify(k)); 576 - // Remove from allDocs 577 576 allDocs = allDocs.filter(d => d.id !== id); 578 577 renderDocuments(); 579 578 }); 580 579 }); 580 + 581 + // Empty all trash 582 + const emptyAllBtn = trashListEl.querySelector('.trash-empty-all'); 583 + if (emptyAllBtn) { 584 + emptyAllBtn.addEventListener('click', async () => { 585 + if (!confirm(`Permanently delete all ${trashedDocs.length} trashed documents? This cannot be undone.`)) return; 586 + const k = JSON.parse(localStorage.getItem('tools-keys') || '{}'); 587 + for (const doc of trashedDocs) { 588 + await fetch(`/api/documents/${doc.id}`, { method: 'DELETE' }).catch(() => {}); 589 + delete k[doc.id]; 590 + } 591 + localStorage.setItem('tools-keys', JSON.stringify(k)); 592 + allDocs = allDocs.filter(d => !trashedDocs.some(t => t.id === d.id)); 593 + trash = []; 594 + localStorage.setItem('tools-trash', JSON.stringify(trash)); 595 + renderDocuments(); 596 + }); 597 + } 581 598 } 582 599 583 600 function showMoveModal(docId: string): void {
+23 -1
src/lib/provider.ts
··· 65 65 _destroyed: boolean; 66 66 _hadSnapshot: boolean; 67 67 _lastSaveTime: number | undefined; 68 + _lastVersionTime: number | undefined; 68 69 69 70 _onDocUpdate: (update: Uint8Array, origin: unknown) => void; 70 71 _onAwarenessUpdate: (payload: AwarenessUpdatePayload) => void; ··· 300 301 try { 301 302 const state = Y.encodeStateAsUpdate(this.doc); 302 303 303 - // Validate: don't save suspiciously small state if we loaded a real snapshot 304 + // Validate: don't save suspiciously small state 305 + // Apply this check if we EVER had meaningful data (loaded or saved) 304 306 if (this._hadSnapshot && state.byteLength < MIN_SNAPSHOT_BYTES) { 305 307 console.warn('Snapshot save skipped: state too small (%d bytes) — possible data loss', state.byteLength); 306 308 return; 307 309 } 308 310 311 + // Auto-create a version for recovery (throttled to max once per 5 minutes) 312 + const VERSION_INTERVAL = 5 * 60 * 1000; 313 + if (state.byteLength >= MIN_SNAPSHOT_BYTES && this._hadSnapshot && 314 + Date.now() - (this._lastVersionTime || 0) > VERSION_INTERVAL) { 315 + try { 316 + const versionEncrypted = await encrypt(state, this.cryptoKey); 317 + await fetch(`${this.apiUrl}/api/documents/${this.roomId}/versions`, { 318 + method: 'POST', 319 + headers: { 'Content-Type': 'application/octet-stream' }, 320 + body: versionEncrypted, 321 + }); 322 + this._lastVersionTime = Date.now(); 323 + } catch { /* version save is best-effort */ } 324 + } 325 + 309 326 const encrypted = await encrypt(state, this.cryptoKey); 310 327 await fetch(`${this.apiUrl}/api/documents/${this.roomId}/snapshot`, { 311 328 method: 'PUT', ··· 313 330 body: encrypted, 314 331 }); 315 332 this._lastSaveTime = Date.now(); 333 + 334 + // Once we've saved meaningful data, protect against future empty saves 335 + if (state.byteLength >= MIN_SNAPSHOT_BYTES) { 336 + this._hadSnapshot = true; 337 + } 316 338 } catch (err: unknown) { 317 339 console.warn('Failed to save snapshot', err); 318 340 }