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]
### 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)

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
- **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!

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="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 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -526,6 +535,8 @@
<p class="help-text">
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.
</p>
</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>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>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>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>

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
- **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):

View File

@@ -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();

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