Files
KeepItGoingServer/static/js/offline-db.js
T
Keith SmithandClaude Sonnet 5 a6f227e931 Bring offline mode to full parity with the online dashboard
Fixes subtasks appearing flattened into the main task list (missing
parent filter), and adds everything else needed for full offline
functionality: tag chips/assignment/creation, full field editing
(description, due time, recurrence presets), time tracking with a
live-updating timer, and properly nested subtasks -- all queued
locally and synced via the existing /api/sync/ protocol.

The offline page now reuses the real dashboard's app-layout/header/
sidebar/detail-pane structure instead of a bespoke layout, with an
amber-tinted header and banner so it's unmistakable which mode you're
in. The detail panel mirrors _task_detail.html's fields and actions.

Also fixes a real backend gap this feature depends on: apply_conflict_data()
in sync/views.py silently no-op'd on time_entry conflicts. And computes
duration_seconds client-side on merge, since TimeEntrySyncSerializer
doesn't include it in the wire format at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 23:45:43 -06:00

119 lines
3.7 KiB
JavaScript

/**
* 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;
}