Files
terra-view/backend/static/offline-db.js

353 lines
10 KiB
JavaScript

/* IndexedDB wrapper for offline data storage in SFM */
/* Handles unit data, status snapshots, and pending edit queue */
class OfflineDB {
constructor() {
this.dbName = 'sfm-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);
}
});
// Export for use in other scripts
export default OfflineDB;