feat: add Progressive Web App support with service worker and offline functionality

This commit is contained in:
2025-11-24 15:04:47 +02:00
parent a3d4fed842
commit fb70db5595
9 changed files with 185 additions and 4 deletions

View File

@@ -99,12 +99,19 @@ Complete feature set for lightweight Vega-Lite snippet management.
## [Unreleased] ## [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 ### Fixed
- (Bugfixes will be listed here) - (Bugfixes will be listed here)
### Added
- (New features will be listed here)
### Changed ### Changed
- (Improvements and refinements will be listed here) - (Improvements and refinements will be listed here)

View File

@@ -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 - **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 - **Draft/published workflow**: Experiment safely without losing your working version
- **Dataset library**: Store and reuse datasets across snippets (JSON, CSV, TSV, TopoJSON) - **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 - **Import/export**: Back up your work or move it between browsers
- **Search and ordering**: Find snippets by name, comment, or spec content - **Search and ordering**: Find snippets by name, comment, or spec content
- **Configurable settings**: Editor options, performance tuning, date formatting, light/dark themes - **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. - **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. - **Experimental dark theme**: Has minor visibility issues in some UI components.
- **No cross-device sync**: Data doesn't sync between browsers or devices. - **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! ### We'd Love Your Feedback!

BIN
icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -8,6 +8,15 @@
<link rel="stylesheet" href="src/styles.css"> <link rel="stylesheet" href="src/styles.css">
<link rel="icon" type="image/svg+xml" href="src/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="src/favicon.svg" />
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#000080">
<!-- iOS PWA Support -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Astrolabe">
<link rel="apple-touch-icon" href="/icon-192x192.png">
<!-- Google Fonts - IBM Plex Mono --> <!-- Google Fonts - IBM Plex Mono -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -526,6 +535,8 @@
<p class="help-text"> <p class="help-text">
Everything runs locally in your browser—no server, no signup, no data leaving your machine. 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. 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.
</p> </p>
</section> </section>
@@ -536,6 +547,7 @@
<li><strong>Three-panel workspace</strong> — Snippet library, Monaco code editor with Vega-Lite schema validation, and live preview</li> <li><strong>Three-panel workspace</strong> — Snippet library, Monaco code editor with Vega-Lite schema validation, and live preview</li>
<li><strong>Draft/published workflow</strong> — Experiment safely without losing your working version</li> <li><strong>Draft/published workflow</strong> — Experiment safely without losing your working version</li>
<li><strong>Dataset library</strong> — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)</li> <li><strong>Dataset library</strong> — Store and reuse datasets across multiple visualizations (supports JSON, CSV, TSV, TopoJSON)</li>
<li><strong>Offline-capable</strong> — Works without internet connection after first visit; install as standalone app</li>
<li><strong>Import/export</strong> — Back up your work or move it between browsers</li> <li><strong>Import/export</strong> — Back up your work or move it between browsers</li>
<li><strong>Inline data extraction</strong> — Convert hardcoded data into reusable datasets</li> <li><strong>Inline data extraction</strong> — Convert hardcoded data into reusable datasets</li>
<li><strong>Search and sorting</strong> — Find snippets by name, comment, or spec content</li> <li><strong>Search and sorting</strong> — Find snippets by name, comment, or spec content</li>

30
manifest.webmanifest Normal file
View File

@@ -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"
}
]
}

View File

@@ -14,6 +14,7 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu
## Design Principles ## Design Principles
- **Local-first**: All data stored in browser (localStorage for snippets, IndexedDB for datasets) - **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 - **Minimal dependencies**: Vanilla JavaScript, no build tools, direct CDN imports
- **Developer-friendly**: Full JSON schema support, syntax validation, and intellisense - **Developer-friendly**: Full JSON schema support, syntax validation, and intellisense
- **Version-aware**: Draft/published workflow for safe experimentation - **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) - **Editor**: Monaco Editor v0.47.0 (via CDN)
- **Visualization**: Vega-Embed v6 (includes Vega v5 & Vega-Lite v5) - **Visualization**: Vega-Embed v6 (includes Vega v5 & Vega-Lite v5)
- **Storage**: localStorage (snippets) + IndexedDB (datasets) - **Storage**: localStorage (snippets) + IndexedDB (datasets)
- **Offline**: Service Worker API with Cache API for PWA functionality
- **Architecture**: Modular script organization with logical file separation - **Architecture**: Modular script organization with logical file separation
- **Backend**: None (frontend-only application) - **Backend**: None (frontend-only application)
@@ -153,6 +155,10 @@ astrolabe:settings # User preferences and UI state
``` ```
web/ web/
├── index.html # Main HTML structure ├── 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/ ├── src/
│ ├── js/ │ ├── js/
│ │ ├── config.js # Global variables, settings API, utilities │ │ ├── config.js # Global variables, settings API, utilities
@@ -230,14 +236,22 @@ web/
- Performance tuning options - Performance tuning options
- Settings modal UI logic - Settings modal UI logic
**app.js** (~250 lines) **app.js** (~270 lines)
- Application initialization sequence - Application initialization sequence
- Service worker registration for PWA
- Event listener registration - Event listener registration
- Monaco editor setup - Monaco editor setup
- URL state management (hashchange listener) - URL state management (hashchange listener)
- Keyboard shortcut handlers - Keyboard shortcut handlers
- Modal management - 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) **styles.css** (~280 lines)
- Windows 2000 aesthetic (classic gray, beveled borders) - Windows 2000 aesthetic (classic gray, beveled borders)
- Component-based architecture (base classes + modifiers) - Component-based architecture (base classes + modifiers)
@@ -363,6 +377,22 @@ const URLState = {
- Page refresh preserves user context - Page refresh preserves user context
- Multi-tab workflows supported - 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 ### Type Detection Algorithm
**Column Type Inference** (for table preview): **Column Type Inference** (for table preview):

View File

@@ -1,5 +1,23 @@
// Application initialization and event handlers // 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 () { document.addEventListener('DOMContentLoaded', function () {
// Initialize user settings // Initialize user settings
initSettings(); initSettings();

82
sw.js Normal file
View File

@@ -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');
}
})
);
});