From fb70db55956e246f188d87fa61efa2a500a9cd6a Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Mon, 24 Nov 2025 15:04:47 +0200 Subject: [PATCH] feat: add Progressive Web App support with service worker and offline functionality --- CHANGELOG.md | 13 ++++-- README.md | 2 + icon-192x192.png | Bin 0 -> 17170 bytes icon-512x512.png | Bin 0 -> 51222 bytes index.html | 12 +++++ manifest.webmanifest | 30 +++++++++++++ project-docs/architecture.md | 32 +++++++++++++- src/js/app.js | 18 ++++++++ sw.js | 82 +++++++++++++++++++++++++++++++++++ 9 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 icon-192x192.png create mode 100644 icon-512x512.png create mode 100644 manifest.webmanifest create mode 100644 sw.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 502f470..c072b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,12 +99,19 @@ Complete feature set for lightweight Vega-Lite snippet management. ## [Unreleased] +### Added +- **Progressive Web App (PWA) Support**: Install Astrolabe as standalone app with offline functionality + - Service worker caches all application files and CDN dependencies + - Full offline access after initial load + - Install button in browser for desktop/mobile installation + - Runs in standalone window without browser chrome + - "Add to Home Screen" support on iOS/Android + - Automatic cache updates when app version changes + - Works seamlessly with existing IndexedDB and localStorage + ### Fixed - (Bugfixes will be listed here) -### Added -- (New features will be listed here) - ### Changed - (Improvements and refinements will be listed here) diff --git a/README.md b/README.md index 78d62d5..1951877 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A lightweight, browser-based snippet manager for Vega-Lite visualizations. Organ - **Visual chart builder**: Create charts without writing JSON – select mark type, map fields to encodings, and see live preview - **Draft/published workflow**: Experiment safely without losing your working version - **Dataset library**: Store and reuse datasets across snippets (JSON, CSV, TSV, TopoJSON) +- **Progressive Web App**: Install as standalone app, works fully offline after first visit - **Import/export**: Back up your work or move it between browsers - **Search and ordering**: Find snippets by name, comment, or spec content - **Configurable settings**: Editor options, performance tuning, date formatting, light/dark themes @@ -90,6 +91,7 @@ A lightweight, browser-based snippet manager for Vega-Lite visualizations. Organ - **Storage limits**: Snippets are limited to 5 MB total (shared localStorage). Datasets use IndexedDB and have much higher limits. - **Experimental dark theme**: Has minor visibility issues in some UI components. - **No cross-device sync**: Data doesn't sync between browsers or devices. +- **Offline functionality**: PWA requires initial online visit to cache resources; subsequent visits work offline. ### We'd Love Your Feedback! diff --git a/icon-192x192.png b/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..82f6d2649ee84e1020a79231dfd333d042e0e4b6 GIT binary patch literal 17170 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 E0V}-am;e9( literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 1bbf359..8138c1f 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,15 @@ + + + + + + + + + @@ -526,6 +535,8 @@

Everything runs locally in your browser—no server, no signup, no data leaving your machine. Your snippets and datasets are stored using browser storage, so they persist across sessions. + As a Progressive Web App, Astrolabe works fully offline after your first visit and can be installed + as a standalone application.

@@ -536,6 +547,7 @@
  • Three-panel workspace — Snippet library, Monaco code editor with Vega-Lite schema validation, and live preview
  • Draft/published workflow — Experiment safely without losing your working version
  • Dataset library — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)
  • +
  • Offline-capable — Works without internet connection after first visit; install as standalone app
  • Import/export — Back up your work or move it between browsers
  • Inline data extraction — Convert hardcoded data into reusable datasets
  • Search and sorting — Find snippets by name, comment, or spec content
  • diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..fb2ed9f --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1,30 @@ +{ + "name": "Astrolabe - Vega-Lite Snippet Manager", + "short_name": "Astrolabe", + "description": "A lightweight, browser-based snippet manager for Vega-Lite visualizations", + "start_url": "/", + "display": "standalone", + "background_color": "#c0c0c0", + "theme_color": "#000080", + "orientation": "any", + "icons": [ + { + "src": "src/favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/project-docs/architecture.md b/project-docs/architecture.md index b9d2eba..10309f6 100644 --- a/project-docs/architecture.md +++ b/project-docs/architecture.md @@ -14,6 +14,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu ## Design Principles - **Local-first**: All data stored in browser (localStorage for snippets, IndexedDB for datasets) +- **Offline-capable**: Progressive Web App with service worker for full offline functionality - **Minimal dependencies**: Vanilla JavaScript, no build tools, direct CDN imports - **Developer-friendly**: Full JSON schema support, syntax validation, and intellisense - **Version-aware**: Draft/published workflow for safe experimentation @@ -28,6 +29,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu - **Editor**: Monaco Editor v0.47.0 (via CDN) - **Visualization**: Vega-Embed v6 (includes Vega v5 & Vega-Lite v5) - **Storage**: localStorage (snippets) + IndexedDB (datasets) +- **Offline**: Service Worker API with Cache API for PWA functionality - **Architecture**: Modular script organization with logical file separation - **Backend**: None (frontend-only application) @@ -153,6 +155,10 @@ astrolabe:settings # User preferences and UI state ``` web/ ├── index.html # Main HTML structure +├── manifest.webmanifest # PWA manifest (app metadata, icons, theme) +├── sw.js # Service worker (offline caching) +├── icon-192x192.png # PWA icon (small) +├── icon-512x512.png # PWA icon (large) ├── src/ │ ├── js/ │ │ ├── config.js # Global variables, settings API, utilities @@ -230,14 +236,22 @@ web/ - Performance tuning options - Settings modal UI logic -**app.js** (~250 lines) +**app.js** (~270 lines) - Application initialization sequence +- Service worker registration for PWA - Event listener registration - Monaco editor setup - URL state management (hashchange listener) - Keyboard shortcut handlers - Modal management +**sw.js** (~90 lines) +- Service worker for Progressive Web App functionality +- Cache management (install, activate, fetch events) +- Offline-first strategy with network fallback +- Automatic cache versioning and cleanup +- CDN resource caching (Monaco, Vega, fonts) + **styles.css** (~280 lines) - Windows 2000 aesthetic (classic gray, beveled borders) - Component-based architecture (base classes + modifiers) @@ -363,6 +377,22 @@ const URLState = { - Page refresh preserves user context - Multi-tab workflows supported +### Progressive Web App Implementation + +Astrolabe uses a service worker to provide offline functionality and installability as a Progressive Web App. + +**Implementation**: +- Service worker (`sw.js`) caches all local files and CDN dependencies (Monaco, Vega, fonts) +- Cache-first strategy with network fallback +- Cache versioning tied to app version for automatic updates +- PWA manifest (`manifest.webmanifest`) defines app metadata, icons, and theme + +**Features**: +- Full offline functionality after first visit +- Browser shows install button for standalone app installation +- Automatic cache updates (checks every 60 seconds) +- Works seamlessly with IndexedDB and localStorage + ### Type Detection Algorithm **Column Type Inference** (for table preview): diff --git a/src/js/app.js b/src/js/app.js index fbdd31f..65ed86a 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,5 +1,23 @@ // Application initialization and event handlers +// Register service worker for PWA functionality +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('Service Worker registered:', registration.scope); + + // Check for updates periodically + setInterval(() => { + registration.update(); + }, 60000); // Check every minute + }) + .catch(error => { + console.warn('Service Worker registration failed:', error); + }); + }); +} + document.addEventListener('DOMContentLoaded', function () { // Initialize user settings initSettings(); diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..f768f41 --- /dev/null +++ b/sw.js @@ -0,0 +1,82 @@ +const CACHE_NAME = 'astrolabe-v1.0.0'; +const URLS_TO_CACHE = [ + '/', + '/index.html', + '/src/styles.css', + '/src/favicon.svg', + '/src/js/config.js', + '/src/js/snippet-manager.js', + '/src/js/dataset-manager.js', + '/src/js/chart-builder.js', + '/src/js/panel-manager.js', + '/src/js/editor.js', + '/src/js/user-settings.js', + '/src/js/generic-storage-ui.js', + '/src/js/app.js' +]; + +// CDN URLs to cache +const CDN_URLS = [ + 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs/loader.js', + 'https://unpkg.com/vega@5/build/vega.min.js', + 'https://unpkg.com/vega-lite@5/build/vega-lite.min.js', + 'https://unpkg.com/vega-embed@6/build/vega-embed.min.js', + 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap' +]; + +// Install event - cache all static assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + // Cache local files + const localCachePromise = cache.addAll(URLS_TO_CACHE); + + // Cache CDN files - they'll be cached during runtime via fetch event + // This avoids CORS issues during install phase + return localCachePromise; + }) + ); + // Force the waiting service worker to become active + self.skipWaiting(); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); + // Take control of all pages immediately + return self.clients.claim(); +}); + +// Fetch event - serve from cache, fallback to network +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then((response) => { + // Return cached version or fetch from network + return response || fetch(event.request).then((fetchResponse) => { + // Cache successful responses for future use + if (fetchResponse && fetchResponse.status === 200) { + const responseToCache = fetchResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + } + return fetchResponse; + }); + }).catch(() => { + // Offline fallback - return cached index for navigation requests + if (event.request.mode === 'navigate') { + return caches.match('/index.html'); + } + }) + ); +});