Full PWA mobile version added, bug fixes on deployment status, navigation links added
This commit is contained in:
352
backend/static/offline-db.js
Normal file
352
backend/static/offline-db.js
Normal file
@@ -0,0 +1,352 @@
|
||||
/* 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;
|
||||
Reference in New Issue
Block a user