feat(frontend): add API client and Svelte stores

- Create API client with auto-retry and token refresh support
- Add comprehensive API endpoints for tasks, tags, projects, sync, and auth
- Implement authStore for authentication state management
- Implement tasksStore with optimistic updates and offline queue
- Add derived stores for filtered task views (pending, completed, by project)
- Implement syncStore for managing sync state and queue
- Add client ID generation and persistence for sync tracking
This commit is contained in:
2026-01-06 15:45:13 +01:00
parent 41795d1827
commit d99e158a8c
5 changed files with 754 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import { writable } from 'svelte/store';
import { sync as syncAPI } from '$lib/api/endpoints.js';
import { getQueue, clearQueue } from '$lib/utils/sync-queue.js';
import { getItem, setItem } from '$lib/utils/storage.js';
import { generateUUID } from '$lib/utils/uuid.js';
/**
* @typedef {import('$lib/api/types.js').SyncResult} SyncResult
*/
/**
* @typedef {Object} SyncState
* @property {'idle'|'syncing'|'error'} status
* @property {number} lastSync - Unix timestamp
* @property {string|null} error
* @property {number} queueSize
* @property {string} clientId
*/
const SYNC_STATE_KEY = 'opal_sync_state';
const CLIENT_ID_KEY = 'opal_client_id';
/**
* Get or create client ID
* @returns {string}
*/
function getClientId() {
let clientId = getItem(CLIENT_ID_KEY);
if (!clientId) {
clientId = generateUUID();
setItem(CLIENT_ID_KEY, clientId);
}
return clientId;
}
/**
* Load sync state
* @returns {SyncState}
*/
function loadSyncState() {
const stored = getItem(SYNC_STATE_KEY);
return {
status: 'idle',
lastSync: stored?.lastSync || 0,
error: null,
queueSize: getQueue().length,
clientId: getClientId()
};
}
/**
* Create sync store
*/
function createSyncStore() {
const { subscribe, set, update } = writable(loadSyncState());
return {
subscribe,
/**
* Perform sync
* @returns {Promise<SyncResult>}
*/
async sync() {
update(state => ({ ...state, status: 'syncing', error: null }));
try {
const state = loadSyncState();
const queue = getQueue();
let result = {
pulled: 0,
pushed: 0,
conflicts_resolved: 0,
queued_offline: 0,
errors: []
};
// Push queued changes
if (queue.length > 0) {
const tasks = queue.map(q => q.data);
try {
await syncAPI.push(tasks, state.clientId);
clearQueue();
result.pushed = queue.length;
} catch (error) {
result.errors.push(`Failed to push queue: ${error.message}`);
}
}
// Pull changes from server
try {
const changes = await syncAPI.getChanges(state.lastSync, state.clientId);
result.pulled = changes.length;
// TODO: Apply changes to local state
} catch (error) {
result.errors.push(`Failed to pull changes: ${error.message}`);
}
// Update sync state
const now = Math.floor(Date.now() / 1000);
setItem(SYNC_STATE_KEY, { lastSync: now });
update(state => ({
...state,
status: 'idle',
lastSync: now,
queueSize: 0,
error: null
}));
return result;
} catch (error) {
update(state => ({
...state,
status: 'error',
error: error.message
}));
throw error;
}
},
/**
* Update queue size
*/
updateQueueSize() {
update(state => ({
...state,
queueSize: getQueue().length
}));
}
};
}
export const syncStore = createSyncStore();