/* IndexedDB wrapper for offline data storage in Terra-View */ /* Handles unit data, status snapshots, and pending edit queue */ class OfflineDB { constructor() { this.dbName = 'terra-view-offline-db'; this.version = 1; this.db = null; } // Initialize database async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.version); request.onerror = () => { console.error('IndexedDB error:', request.error); reject(request.error); }; request.onsuccess = () => { this.db = request.result; console.log('IndexedDB initialized successfully'); resolve(this.db); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Units store - full unit details if (!db.objectStoreNames.contains('units')) { const unitsStore = db.createObjectStore('units', { keyPath: 'id' }); unitsStore.createIndex('device_type', 'device_type', { unique: false }); unitsStore.createIndex('deployed', 'deployed', { unique: false }); console.log('Created units object store'); } // Status snapshot store - latest status data if (!db.objectStoreNames.contains('status-snapshot')) { db.createObjectStore('status-snapshot', { keyPath: 'timestamp' }); console.log('Created status-snapshot object store'); } // Pending edits store - offline edit queue if (!db.objectStoreNames.contains('pending-edits')) { const editsStore = db.createObjectStore('pending-edits', { keyPath: 'id', autoIncrement: true }); editsStore.createIndex('unitId', 'unitId', { unique: false }); editsStore.createIndex('timestamp', 'timestamp', { unique: false }); console.log('Created pending-edits object store'); } }; }); } // ===== UNITS OPERATIONS ===== // Save or update a unit async saveUnit(unit) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['units'], 'readwrite'); const store = transaction.objectStore('units'); const request = store.put(unit); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Get a single unit by ID async getUnit(unitId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['units'], 'readonly'); const store = transaction.objectStore('units'); const request = store.get(unitId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Get all units async getAllUnits() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['units'], 'readonly'); const store = transaction.objectStore('units'); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Delete a unit async deleteUnit(unitId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['units'], 'readwrite'); const store = transaction.objectStore('units'); const request = store.delete(unitId); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } // ===== STATUS SNAPSHOT OPERATIONS ===== // Save status snapshot async saveSnapshot(snapshot) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['status-snapshot'], 'readwrite'); const store = transaction.objectStore('status-snapshot'); // Add timestamp const snapshotWithTimestamp = { ...snapshot, timestamp: Date.now() }; const request = store.put(snapshotWithTimestamp); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Get latest status snapshot async getLatestSnapshot() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['status-snapshot'], 'readonly'); const store = transaction.objectStore('status-snapshot'); const request = store.getAll(); request.onsuccess = () => { const snapshots = request.result; if (snapshots.length > 0) { // Return the most recent snapshot const latest = snapshots.reduce((prev, current) => (prev.timestamp > current.timestamp) ? prev : current ); resolve(latest); } else { resolve(null); } }; request.onerror = () => reject(request.error); }); } // Clear old snapshots (keep only latest) async clearOldSnapshots() { if (!this.db) await this.init(); return new Promise(async (resolve, reject) => { const transaction = this.db.transaction(['status-snapshot'], 'readwrite'); const store = transaction.objectStore('status-snapshot'); const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { const snapshots = getAllRequest.result; if (snapshots.length > 1) { // Sort by timestamp, keep only the latest snapshots.sort((a, b) => b.timestamp - a.timestamp); // Delete all except the first (latest) for (let i = 1; i < snapshots.length; i++) { store.delete(snapshots[i].timestamp); } } resolve(); }; getAllRequest.onerror = () => reject(getAllRequest.error); }); } // ===== PENDING EDITS OPERATIONS ===== // Queue an edit for offline sync async queueEdit(unitId, changes) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['pending-edits'], 'readwrite'); const store = transaction.objectStore('pending-edits'); const edit = { unitId, changes, timestamp: Date.now() }; const request = store.add(edit); request.onsuccess = () => { console.log(`Queued edit for unit ${unitId}`); resolve(request.result); }; request.onerror = () => reject(request.error); }); } // Get all pending edits async getPendingEdits() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['pending-edits'], 'readonly'); const store = transaction.objectStore('pending-edits'); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Get pending edits count async getPendingEditsCount() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['pending-edits'], 'readonly'); const store = transaction.objectStore('pending-edits'); const request = store.count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Clear a synced edit async clearEdit(editId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['pending-edits'], 'readwrite'); const store = transaction.objectStore('pending-edits'); const request = store.delete(editId); request.onsuccess = () => { console.log(`Cleared edit ${editId} from queue`); resolve(); }; request.onerror = () => reject(request.error); }); } // Clear all pending edits async clearAllEdits() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['pending-edits'], 'readwrite'); const store = transaction.objectStore('pending-edits'); const request = store.clear(); request.onsuccess = () => { console.log('Cleared all pending edits'); resolve(); }; request.onerror = () => reject(request.error); }); } // ===== UTILITY OPERATIONS ===== // Clear all data (for debugging/reset) async clearAllData() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const storeNames = ['units', 'status-snapshot', 'pending-edits']; const transaction = this.db.transaction(storeNames, 'readwrite'); storeNames.forEach(storeName => { transaction.objectStore(storeName).clear(); }); transaction.oncomplete = () => { console.log('Cleared all offline data'); resolve(); }; transaction.onerror = () => reject(transaction.error); }); } // Get database statistics async getStats() { if (!this.db) await this.init(); const unitsCount = await new Promise((resolve, reject) => { const transaction = this.db.transaction(['units'], 'readonly'); const request = transaction.objectStore('units').count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const pendingEditsCount = await this.getPendingEditsCount(); const hasSnapshot = await new Promise((resolve, reject) => { const transaction = this.db.transaction(['status-snapshot'], 'readonly'); const request = transaction.objectStore('status-snapshot').count(); request.onsuccess = () => resolve(request.result > 0); request.onerror = () => reject(request.error); }); return { unitsCount, pendingEditsCount, hasSnapshot }; } } // Create global instance window.offlineDB = new OfflineDB(); // Initialize on page load document.addEventListener('DOMContentLoaded', async () => { try { await window.offlineDB.init(); console.log('Offline database ready'); // Display pending edits count if any const pendingCount = await window.offlineDB.getPendingEditsCount(); if (pendingCount > 0) { console.log(`${pendingCount} pending edits in queue`); // Could show a badge in the UI here } } catch (error) { console.error('Failed to initialize offline database:', error); } });