Internal
Public Access
Add offline task CRUD, phase 1 (PWA)
Lets users create, edit, and complete tasks with no network connection, queued locally in IndexedDB and replayed via the existing /api/sync/ protocol once back online -- the same protocol the native Android app already uses, with zero backend changes. The service worker now routes the dashboard's offline fallback to a small client-rendered task app instead of the generic "you're offline" page; every other route keeps the generic fallback. Conflicts (server row touched elsewhere since last sync) auto-resolve as "local wins" rather than surfacing a resolution UI. Scope is title/ status/priority/due-date only -- tags, subtasks, time tracking, and recurrence editing offline are out of scope for this phase. Also adds the first real test coverage for sync/views.py (previously untested): new-item creation, soft-delete-bypasses-conflict-check, conflict detection/discarding, and both resolve_conflict paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9ac1c41897
commit
922d4d1bf2
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 = 1;
|
||||
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user