353 lines
10 KiB
JavaScript
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;
|