/** * KeepItGoing - Offline IndexedDB helpers * Pure local storage access for offline task data. No network calls here — * see offline-sync.js for the /api/sync/ replay logic. */ const OFFLINE_DB_NAME = 'keepitgoing-offline'; const OFFLINE_DB_VERSION = 2; function openOfflineDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(OFFLINE_DB_NAME, OFFLINE_DB_VERSION); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('tasks')) { db.createObjectStore('tasks', { keyPath: 'sync_id' }); } if (!db.objectStoreNames.contains('meta')) { db.createObjectStore('meta', { keyPath: 'key' }); } if (!db.objectStoreNames.contains('tags')) { db.createObjectStore('tags', { keyPath: 'sync_id' }); } if (!db.objectStoreNames.contains('time_entries')) { db.createObjectStore('time_entries', { keyPath: 'sync_id' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getMeta(key) { const db = await openOfflineDB(); const tx = db.transaction('meta', 'readonly'); const result = await promisifyRequest(tx.objectStore('meta').get(key)); return result ? result.value : undefined; } async function setMeta(key, value) { const db = await openOfflineDB(); const tx = db.transaction('meta', 'readwrite'); await promisifyRequest(tx.objectStore('meta').put({ key, value })); } async function ensureDeviceId() { let deviceId = await getMeta('device_id'); if (!deviceId) { deviceId = 'web-' + crypto.randomUUID(); await setMeta('device_id', deviceId); } return deviceId; } async function getAllTasks() { const db = await openOfflineDB(); const tx = db.transaction('tasks', 'readonly'); return promisifyRequest(tx.objectStore('tasks').getAll()); } async function putLocalTask(task) { const db = await openOfflineDB(); const tx = db.transaction('tasks', 'readwrite'); await promisifyRequest(tx.objectStore('tasks').put(task)); } async function getDirtyTasks() { const tasks = await getAllTasks(); return tasks.filter((t) => t._dirty === true); } async function getAllTags() { const db = await openOfflineDB(); const tx = db.transaction('tags', 'readonly'); return promisifyRequest(tx.objectStore('tags').getAll()); } async function putLocalTag(tag) { const db = await openOfflineDB(); const tx = db.transaction('tags', 'readwrite'); await promisifyRequest(tx.objectStore('tags').put(tag)); } async function getDirtyTags() { const tags = await getAllTags(); return tags.filter((t) => t._dirty === true); } async function getAllTimeEntries() { const db = await openOfflineDB(); const tx = db.transaction('time_entries', 'readonly'); return promisifyRequest(tx.objectStore('time_entries').getAll()); } async function putLocalTimeEntry(entry) { const db = await openOfflineDB(); const tx = db.transaction('time_entries', 'readwrite'); await promisifyRequest(tx.objectStore('time_entries').put(entry)); } async function getDirtyTimeEntries() { const entries = await getAllTimeEntries(); return entries.filter((e) => e._dirty === true); } async function getRunningTimeEntry() { const entries = await getAllTimeEntries(); return entries.find((e) => !e.is_deleted && !e.ended_at) || null; }