From eaf14aafdda495944eaa7d2cc391508368a20822 Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Mon, 13 Oct 2025 13:50:12 +0300 Subject: [PATCH] feat: add storage monitor to track localStorage usage --- CLAUDE.md | 5 +++- docs/dev-plan.md | 28 ++++++++++-------- index.html | 9 ++++++ src/js/app.js | 3 ++ src/js/snippet-manager.js | 50 +++++++++++++++++++++++++++++++- src/styles.css | 61 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 139 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 59678e4..e2e6dd3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,4 +15,7 @@ Instructions for Claude Code when working on this project. ## Current Status -refer `docs/dev-plan.md` for the current status. +**Completed**: Phases 0-8 (All core functionality including storage monitoring) +**Next**: Phase 9 - Export/Import + +See `docs/dev-plan.md` for complete roadmap and technical details. diff --git a/docs/dev-plan.md b/docs/dev-plan.md index 07e48de..a3cc675 100644 --- a/docs/dev-plan.md +++ b/docs/dev-plan.md @@ -156,16 +156,17 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu --- -### **Phase 8: Storage Monitoring** +### **Phase 8: Storage Monitoring** ✅ **COMPLETE** **Goal**: Show storage usage and limits -- [ ] Calculate total localStorage usage -- [ ] Display as progress bar or text (e.g., "2.3 MB / ~5 MB") -- [ ] Show individual snippet sizes in list -- [ ] Add warning indicator when approaching 80% capacity -- [ ] Display draft vs published size differences - -**Deliverable**: User can see storage consumption +**Deliverables**: +- Storage usage calculation using Blob API for accurate byte counting +- Progress bar display showing usage vs 5MB limit (e.g., "2.3 MB / 5 MB") +- Visual warning states: green (normal), orange (90%+ warning), red (95%+ critical) +- Storage monitor positioned at bottom of snippet panel below metadata +- Automatic updates after every save operation +- Alert dialog when localStorage quota is exceeded +- Flexbox layout ensuring monitor stays at panel bottom with scrollable snippet list --- @@ -304,15 +305,15 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu ## Current Status -**Completed**: Phases 0-7 (Storage, UI, editor, rendering, persistence, CRUD, organization, draft/published workflow) -**Active**: Phase 8 - Storage Monitoring +**Completed**: Phases 0-8 (Storage, UI, editor, rendering, persistence, CRUD, organization, draft/published workflow, storage monitoring) +**Next**: Phase 9 - Export/Import **See**: `CLAUDE.md` for concise current state summary --- ## Implemented Features -### Core Capabilities (Phases 0-7) +### Core Capabilities (Phases 0-8) - Three-panel resizable layout with memory and persistence - Monaco Editor v0.47.0 with Vega-Lite v5 schema validation - Live Vega-Lite rendering with debounced updates and error display @@ -324,6 +325,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu - Draft/Published workflow with version control - Status indicator lights (green/yellow) showing draft state - Context-aware Publish/Revert buttons with color coding +- Storage monitoring with visual progress bar and warning states - Retro Windows 2000 aesthetic throughout ### Technical Implementation @@ -334,4 +336,6 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu - **Debouncing**: 1.5s render, 1s auto-save, 300ms search - **AMD Resolution**: Temporary `window.define` disabling for Vega library loading - **Panel Memory**: localStorage persistence for sizes and visibility across sessions -- **Data Model**: Phase 0 schema with `spec` (published) and `draftSpec` (working) fields \ No newline at end of file +- **Data Model**: Phase 0 schema with `spec` (published) and `draftSpec` (working) fields +- **Storage Calculation**: Blob API for accurate byte counting of snippet data +- **Flexbox Layout**: Scrollable snippet list with fixed metadata and storage monitor at bottom \ No newline at end of file diff --git a/index.html b/index.html index 1e059e3..4b77815 100644 --- a/index.html +++ b/index.html @@ -94,6 +94,15 @@ +
+
+ Storage: + 0 KB / 5 MB +
+
+
+
+
diff --git a/src/js/app.js b/src/js/app.js index bfe532d..c6d41ff 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -12,6 +12,9 @@ document.addEventListener('DOMContentLoaded', function () { renderSnippetList(); + // Update storage monitor + updateStorageMonitor(); + // Auto-select first snippet on page load const firstSnippet = SnippetStorage.listSnippets()[0]; if (firstSnippet) { diff --git a/src/js/snippet-manager.js b/src/js/snippet-manager.js index e97afd3..4cc0ac0 100644 --- a/src/js/snippet-manager.js +++ b/src/js/snippet-manager.js @@ -1,5 +1,9 @@ // Snippet management and localStorage functionality +// Storage limits (5MB in bytes) +const STORAGE_LIMIT_BYTES = 5 * 1024 * 1024; +const WARNING_THRESHOLD = 0.9; // 90% = 4.5MB + // Generate unique ID using Date.now() + random numbers function generateSnippetId() { return Date.now() + Math.random() * 1000; @@ -39,10 +43,11 @@ const SnippetStorage = { saveSnippets(snippets) { try { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(snippets)); + updateStorageMonitor(); return true; } catch (error) { console.error('Failed to save snippets to localStorage:', error); - // TODO: Handle quota exceeded, show user error + alert('Failed to save: Storage quota may be exceeded. Consider deleting old snippets.'); return false; } }, @@ -798,3 +803,46 @@ function revertDraft() { } } +// Calculate storage usage in bytes +function calculateStorageUsage() { + const snippetsData = localStorage.getItem(SnippetStorage.STORAGE_KEY); + if (!snippetsData) return 0; + + // Calculate size in bytes + return new Blob([snippetsData]).size; +} + +// Format bytes to human-readable size +function formatBytes(bytes) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} + +// Update storage monitor display +function updateStorageMonitor() { + const usedBytes = calculateStorageUsage(); + const percentage = (usedBytes / STORAGE_LIMIT_BYTES) * 100; + + const storageText = document.getElementById('storage-text'); + const storageFill = document.getElementById('storage-fill'); + + if (storageText) { + storageText.textContent = `${formatBytes(usedBytes)} / 5 MB`; + } + + if (storageFill) { + storageFill.style.width = `${Math.min(percentage, 100)}%`; + + // Remove all state classes + storageFill.classList.remove('warning', 'critical'); + + // Add warning/critical classes based on usage + if (percentage >= 95) { + storageFill.classList.add('critical'); + } else if (percentage >= 90) { + storageFill.classList.add('warning'); + } + } +} + diff --git a/src/styles.css b/src/styles.css index adec5f4..7eaced8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -332,9 +332,11 @@ body { .panel-content { flex: 1; padding: 8px; - overflow: auto; + overflow: hidden; background: #ffffff; border: 1px inset #c0c0c0; + display: flex; + flex-direction: column; } /* Panel sizing */ @@ -392,6 +394,10 @@ body { .snippet-list { list-style: none; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + margin-bottom: 8px; } .snippet-item { @@ -477,13 +483,14 @@ body { /* Snippet meta section */ .snippet-meta { margin-top: 12px; - padding: 8px; + padding: 8px 8px 16px 8px; border-top: 1px solid #808080; background: #f0f0f0; border: 1px inset #c0c0c0; margin-left: -8px; margin-right: -8px; - margin-bottom: -8px; + margin-bottom: 0; + flex-shrink: 0; } .meta-header { @@ -598,4 +605,52 @@ body { .ghost-card .snippet-date { color: #808080; +} + +/* Storage monitor */ +.storage-monitor { + padding: 8px; + background: #f0f0f0; + border-top: 1px solid #808080; + margin: 0 -8px -8px -8px; + flex-shrink: 0; +} + +.storage-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; + font-size: 10px; +} + +.storage-label { + font-weight: bold; + color: #000000; +} + +.storage-text { + color: #606060; +} + +.storage-bar { + width: 100%; + height: 12px; + background: #ffffff; + border: 1px inset #c0c0c0; + position: relative; +} + +.storage-fill { + height: 100%; + background: #00aa00; + transition: width 0.3s ease, background-color 0.3s ease; +} + +.storage-fill.warning { + background: #ff8800; +} + +.storage-fill.critical { + background: #ff0000; } \ No newline at end of file