diff --git a/opal-web/src/lib/api/client.js b/opal-web/src/lib/api/client.js index 49b191a..93220b4 100644 --- a/opal-web/src/lib/api/client.js +++ b/opal-web/src/lib/api/client.js @@ -6,7 +6,7 @@ import { get } from 'svelte/store'; * @typedef {import('./types.js').AuthTokens} AuthTokens */ -const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080'; +export const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080'; /** * Make authenticated API request @@ -23,7 +23,6 @@ export async function apiRequest(endpoint, options = {}) { ...options.headers }; - // Add auth token if available if (auth.accessToken) { headers['Authorization'] = `Bearer ${auth.accessToken}`; } @@ -34,11 +33,9 @@ export async function apiRequest(endpoint, options = {}) { headers }); - // Token expired - try refresh if (response.status === 401 && auth.refreshToken) { const refreshed = await refreshAccessToken(auth.refreshToken); if (refreshed) { - // Retry with new token headers['Authorization'] = `Bearer ${refreshed.access_token}`; return apiRequest(endpoint, { ...options, headers }); } @@ -78,7 +75,6 @@ async function refreshAccessToken(refreshToken) { const result = await response.json(); if (result.success) { - // Update auth store authStore.setTokens(result.data); return result.data; } diff --git a/opal-web/src/lib/api/endpoints.js b/opal-web/src/lib/api/endpoints.js index 6801849..22ae267 100644 --- a/opal-web/src/lib/api/endpoints.js +++ b/opal-web/src/lib/api/endpoints.js @@ -1,4 +1,4 @@ -import { apiRequest } from './client.js'; +import { apiRequest, API_BASE } from './client.js'; /** * @typedef {import('./types.js').Task} Task @@ -8,12 +8,8 @@ import { apiRequest } from './client.js'; * @typedef {import('./types.js').User} User */ -const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080'; - -// Tasks API export const tasks = { /** - * List all tasks with optional filters * @param {TaskFilters} [filters] * @returns {Promise} */ @@ -33,20 +29,12 @@ export const tasks = { return apiRequest(`/tasks${query ? `?${query}` : ''}`); }, - /** - * Get single task by UUID - * @param {string} uuid - * @returns {Promise} - */ + /** @param {string} uuid @returns {Promise} */ async get(uuid) { return apiRequest(`/tasks/${uuid}`); }, - /** - * Create new task - * @param {Partial} task - * @returns {Promise} - */ + /** @param {Partial} task @returns {Promise} */ async create(task) { return apiRequest('/tasks', { method: 'POST', @@ -55,7 +43,6 @@ export const tasks = { }, /** - * Update existing task * @param {string} uuid * @param {Partial} updates * @returns {Promise} @@ -67,44 +54,27 @@ export const tasks = { }); }, - /** - * Delete task - * @param {string} uuid - * @returns {Promise} - */ + /** @param {string} uuid @returns {Promise} */ async delete(uuid) { return apiRequest(`/tasks/${uuid}`, { method: 'DELETE' }); }, - /** - * Complete task - * @param {string} uuid - * @returns {Promise} - */ + /** @param {string} uuid @returns {Promise} */ async complete(uuid) { return apiRequest(`/tasks/${uuid}/complete`, { method: 'POST' }); }, - /** - * Start task timer - * @param {string} uuid - * @returns {Promise} - */ + /** @param {string} uuid @returns {Promise} */ async start(uuid) { return apiRequest(`/tasks/${uuid}/start`, { method: 'POST' }); }, - - /** - * Stop task timer - * @param {string} uuid - * @returns {Promise} - */ + + /** @param {string} uuid @returns {Promise} */ async stop(uuid) { return apiRequest(`/tasks/${uuid}/stop`, { method: 'POST' }); }, /** - * Parse CLI input and create task * @param {string} input - Raw opal CLI syntax * @returns {Promise} */ @@ -115,29 +85,20 @@ export const tasks = { }); }, - /** - * List tasks by report name - * @param {string} reportName - * @returns {Promise} - */ + /** @param {string} reportName @returns {Promise} */ async listByReport(reportName) { const result = await apiRequest(`/tasks?report=${encodeURIComponent(reportName)}`); return result.tasks ?? result; } }; -// Tags API export const tags = { - /** - * List all unique tags - * @returns {Promise} - */ + /** @returns {Promise} */ async list() { return apiRequest('/tags'); }, /** - * Add tag to task * @param {string} uuid * @param {string} tag * @returns {Promise} @@ -150,7 +111,6 @@ export const tags = { }, /** - * Remove tag from task * @param {string} uuid * @param {string} tag * @returns {Promise} @@ -162,21 +122,15 @@ export const tags = { } }; -// Projects API export const projects = { - /** - * List all projects - * @returns {Promise} - */ + /** @returns {Promise} */ async list() { return apiRequest('/projects'); } }; -// Sync API export const sync = { /** - * Get changes since timestamp * @param {number} since - Unix timestamp * @param {string} clientId * @returns {Promise} @@ -189,7 +143,6 @@ export const sync = { }, /** - * Push local changes to server * @param {Task[]} tasks * @param {string} clientId * @returns {Promise<{processed: number, conflicts: number}>} @@ -202,12 +155,8 @@ export const sync = { } }; -// Auth API export const auth = { - /** - * Get OAuth login URL - * @returns {Promise<{url: string, state: string}>} - */ + /** @returns {Promise<{url: string, state: string}>} */ async getLoginUrl() { const response = await fetch(`${API_BASE}/auth/login`); const result = await response.json(); @@ -218,7 +167,6 @@ export const auth = { }, /** - * Exchange OAuth code for tokens * @param {string} code * @returns {Promise<{access_token: string, refresh_token: string, expires_at: number, token_type: string, user: User}>} */ @@ -236,11 +184,7 @@ export const auth = { return result.data; }, - /** - * Logout (revoke refresh token) - * @param {string} refreshToken - * @returns {Promise} - */ + /** @param {string} refreshToken @returns {Promise} */ async logout(refreshToken) { return apiRequest('/auth/logout', { method: 'POST', diff --git a/opal-web/src/lib/components/BottomSheet.svelte b/opal-web/src/lib/components/BottomSheet.svelte index acbe6f4..00af54e 100644 --- a/opal-web/src/lib/components/BottomSheet.svelte +++ b/opal-web/src/lib/components/BottomSheet.svelte @@ -29,7 +29,6 @@ /** @type {HTMLDivElement|null} */ let sheetEl = null; - // Body scroll lock — managed in afterUpdate to avoid SSR document access afterUpdate(() => { if (!mounted) return; if (open) { @@ -51,7 +50,6 @@ e.preventDefault(); onClose(); } - // Focus trap if (e.key === 'Tab' && sheetEl) { const focusable = sheetEl.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' @@ -115,10 +113,8 @@ if (dragging) { if (e.cancelable) e.preventDefault(); - // Only allow dragging down (positive) dragOffset = Math.max(0, deltaY); - // Track velocity const now = Date.now(); const dt = now - lastTime; if (dt > 0 && lastY !== null) { diff --git a/opal-web/src/lib/components/FilterPills.svelte b/opal-web/src/lib/components/FilterPills.svelte index 6057995..b330ea0 100644 --- a/opal-web/src/lib/components/FilterPills.svelte +++ b/opal-web/src/lib/components/FilterPills.svelte @@ -1,11 +1,7 @@ - -
- {#if label} - - {/if} - - -
- - diff --git a/opal-web/src/lib/index.js b/opal-web/src/lib/index.js deleted file mode 100644 index 856f2b6..0000000 --- a/opal-web/src/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/opal-web/src/lib/stores/auth.js b/opal-web/src/lib/stores/auth.js index de3340d..3e74794 100644 --- a/opal-web/src/lib/stores/auth.js +++ b/opal-web/src/lib/stores/auth.js @@ -19,10 +19,16 @@ import { getItem, setItem, removeItem } from '$lib/utils/storage.js'; const STORAGE_KEY = 'opal_auth'; const DEV_MODE = import.meta.env.DEV; -/** - * Load auth state from localStorage - * @returns {AuthState} - */ +/** @type {AuthState} */ +const EMPTY_STATE = { + accessToken: null, + refreshToken: null, + expiresAt: null, + user: null, + isAuthenticated: false +}; + +/** @returns {AuthState} */ function loadAuth() { // In dev mode, auto-authenticate with a dev user. // API requests still go to the real backend (which runs with auth disabled). @@ -36,21 +42,11 @@ function loadAuth() { }; } - if (!browser) { - return { - accessToken: null, - refreshToken: null, - expiresAt: null, - user: null, - isAuthenticated: false - }; - } + if (!browser) return EMPTY_STATE; const stored = getItem(STORAGE_KEY); if (stored) { - // Check if token expired if (stored.expiresAt && stored.expiresAt < Date.now() / 1000) { - // Token expired - clear removeItem(STORAGE_KEY); return loadAuth(); } @@ -60,28 +56,21 @@ function loadAuth() { }; } - return { - accessToken: null, - refreshToken: null, - expiresAt: null, - user: null, - isAuthenticated: false - }; + return EMPTY_STATE; } -/** - * Create auth store - */ function createAuthStore() { const { subscribe, set, update } = writable(loadAuth()); + /** Persist state to localStorage */ + function persist(/** @type {AuthState} */ state) { + if (browser) setItem(STORAGE_KEY, state); + } + return { subscribe, - /** - * Set authentication tokens - * @param {AuthTokens} tokens - */ + /** @param {AuthTokens} tokens */ setTokens(tokens) { update(state => { const newState = { @@ -91,33 +80,21 @@ function createAuthStore() { expiresAt: tokens.expires_at, isAuthenticated: true }; - - if (browser) { - setItem(STORAGE_KEY, newState); - } - + persist(newState); return newState; }); }, - /** - * Set user info - * @param {User} user - */ + /** @param {User} user */ setUser(user) { update(state => { const newState = { ...state, user }; - if (browser) { - setItem(STORAGE_KEY, newState); - } + persist(newState); return newState; }); }, - /** - * Set full auth data (tokens + user) - * @param {AuthTokens & {user: User}} data - */ + /** @param {AuthTokens & {user: User}} data */ setAuth(data) { const newState = { accessToken: data.access_token, @@ -126,28 +103,13 @@ function createAuthStore() { user: data.user, isAuthenticated: true }; - - if (browser) { - setItem(STORAGE_KEY, newState); - } - + persist(newState); set(newState); }, - /** - * Clear auth (logout) - */ clear() { - if (browser) { - removeItem(STORAGE_KEY); - } - set({ - accessToken: null, - refreshToken: null, - expiresAt: null, - user: null, - isAuthenticated: false - }); + if (browser) removeItem(STORAGE_KEY); + set(EMPTY_STATE); } }; } diff --git a/opal-web/src/lib/stores/filters.js b/opal-web/src/lib/stores/filters.js index 9826d9a..3a145f1 100644 --- a/opal-web/src/lib/stores/filters.js +++ b/opal-web/src/lib/stores/filters.js @@ -6,7 +6,6 @@ const RECENT_KEY = 'opal_recent_filters'; const MAX_RECENT = 8; /** - * Create a localStorage-backed writable store * @template T * @param {string} key * @param {T} fallback @@ -21,10 +20,7 @@ function persisted(key, fallback) { export const activeFilter = persisted(ACTIVE_KEY, /** @type {string|null} */ (null)); export const recentFilters = persisted(RECENT_KEY, /** @type {string[]} */ ([])); -/** - * Set the active filter and add it to recents - * @param {string} str - */ +/** @param {string} str */ export function setFilter(str) { const trimmed = str.trim(); if (!trimmed) { @@ -39,17 +35,11 @@ export function setFilter(str) { }); } -/** - * Clear the active filter - */ export function clearFilter() { activeFilter.set(null); } -/** - * Remove a specific entry from recents - * @param {string} str - */ +/** @param {string} str */ export function removeRecent(str) { recentFilters.update(recents => recents.filter(r => r !== str)); } diff --git a/opal-web/src/lib/stores/sync.js b/opal-web/src/lib/stores/sync.js index 7f40df2..71be2dc 100644 --- a/opal-web/src/lib/stores/sync.js +++ b/opal-web/src/lib/stores/sync.js @@ -20,10 +20,7 @@ import { generateUUID } from '$lib/utils/uuid.js'; const SYNC_STATE_KEY = 'opal_sync_state'; const CLIENT_ID_KEY = 'opal_client_id'; -/** - * Get or create client ID - * @returns {string} - */ +/** @returns {string} */ function getClientId() { let clientId = getItem(CLIENT_ID_KEY); if (!clientId) { @@ -33,10 +30,7 @@ function getClientId() { return clientId; } -/** - * Load sync state - * @returns {SyncState} - */ +/** @returns {SyncState} */ function loadSyncState() { const stored = getItem(SYNC_STATE_KEY); return { @@ -48,26 +42,20 @@ function loadSyncState() { }; } -/** - * Create sync store - */ function createSyncStore() { const { subscribe, set, update } = writable(loadSyncState()); - + return { subscribe, - - /** - * Perform sync - * @returns {Promise} - */ + + /** @returns {Promise} */ async sync() { update(state => ({ ...state, status: 'syncing', error: null })); - + try { const state = loadSyncState(); const queue = getQueue(); - + let result = { pulled: 0, pushed: 0, @@ -75,8 +63,7 @@ function createSyncStore() { queued_offline: 0, errors: [] }; - - // Push queued changes + if (queue.length > 0) { const tasks = queue.map(q => q.data); try { @@ -87,8 +74,7 @@ function createSyncStore() { 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; @@ -96,11 +82,10 @@ function createSyncStore() { } 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', @@ -108,7 +93,7 @@ function createSyncStore() { queueSize: 0, error: null })); - + return result; } catch (error) { update(state => ({ @@ -119,10 +104,7 @@ function createSyncStore() { throw error; } }, - - /** - * Update queue size - */ + updateQueueSize() { update(state => ({ ...state, diff --git a/opal-web/src/lib/stores/tasks.js b/opal-web/src/lib/stores/tasks.js index e26684d..91dd433 100644 --- a/opal-web/src/lib/stores/tasks.js +++ b/opal-web/src/lib/stores/tasks.js @@ -7,17 +7,22 @@ import { queueChange } from '$lib/utils/sync-queue.js'; * @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters */ -/** - * Create tasks store - */ function createTasksStore() { const { subscribe, set, update } = writable(/** @type {Task[]} */ ([])); + /** + * Replace a single task in the array by UUID. + * @param {string} uuid + * @param {(task: Task) => Task} fn + */ + function updateByUuid(uuid, fn) { + update(tasks => tasks.map(t => t.uuid === uuid ? fn(t) : t)); + } + return { subscribe, /** - * Load tasks by report name * @param {string} reportName - Backend report name (e.g. 'list', 'next', 'completed') */ async loadReport(reportName) { @@ -31,7 +36,6 @@ function createTasksStore() { }, /** - * Parse CLI input and create a task * @param {string} input - Raw opal CLI syntax * @returns {Promise} */ @@ -47,10 +51,7 @@ function createTasksStore() { } }, - /** - * Load all tasks from API - * @param {TaskFilters} [filters] - */ + /** @param {TaskFilters} [filters] */ async load(filters = {}) { try { const tasks = await tasksAPI.list(filters); @@ -62,7 +63,7 @@ function createTasksStore() { }, /** - * Add new task (optimistic update) + * Optimistic create — queues offline on failure. * @param {Partial} task */ async add(task) { @@ -71,101 +72,55 @@ function createTasksStore() { update(tasks => [...tasks, created]); return created; } catch (error) { - // Queue for offline sync - queueChange({ - type: 'create', - task_uuid: task.uuid, - data: task - }); - - // Still update UI optimistically + queueChange({ type: 'create', task_uuid: task.uuid, data: task }); update(tasks => [...tasks, task]); throw error; } }, /** - * Update task (optimistic update) + * Optimistic update — queues offline on failure. * @param {string} uuid * @param {Partial} updates */ async updateTask(uuid, updates) { - // Optimistic update - update(tasks => { - const index = tasks.findIndex(t => t.uuid === uuid); - if (index >= 0) { - tasks[index] = { ...tasks[index], ...updates, modified: Date.now() / 1000 }; - } - return tasks; - }); + updateByUuid(uuid, t => ({ ...t, ...updates, modified: Date.now() / 1000 })); try { const updated = await tasksAPI.update(uuid, updates); - // Sync with server response - update(tasks => { - const index = tasks.findIndex(t => t.uuid === uuid); - if (index >= 0) { - tasks[index] = updated; - } - return tasks; - }); + updateByUuid(uuid, () => updated); } catch (error) { - // Queue for offline sync - queueChange({ - type: 'update', - task_uuid: uuid, - data: updates - }); + queueChange({ type: 'update', task_uuid: uuid, data: updates }); throw error; } }, - /** - * Delete task - * @param {string} uuid - */ + /** @param {string} uuid */ async deleteTask(uuid) { - // Optimistic removal update(tasks => tasks.filter(t => t.uuid !== uuid)); try { await tasksAPI.delete(uuid); } catch (error) { - queueChange({ - type: 'delete', - task_uuid: uuid, - data: {} - }); + queueChange({ type: 'delete', task_uuid: uuid, data: {} }); throw error; } }, - /** - * Start task timer (optimistic) - * @param {string} uuid - */ + /** @param {string} uuid */ async startTask(uuid) { const now = Math.floor(Date.now() / 1000); - update(tasks => tasks.map(t => - t.uuid === uuid ? { ...t, start: now } : t - )); + updateByUuid(uuid, t => ({ ...t, start: now })); try { const updated = await tasksAPI.start(uuid); - update(tasks => tasks.map(t => - t.uuid === uuid ? updated : t - )); + updateByUuid(uuid, () => updated); } catch (error) { - update(tasks => tasks.map(t => - t.uuid === uuid ? { ...t, start: null } : t - )); + updateByUuid(uuid, t => ({ ...t, start: null })); throw error; } }, - /** - * Stop task timer (optimistic) - * @param {string} uuid - */ + /** @param {string} uuid */ async stopTask(uuid) { /** @type {number|null} */ let prevStart = null; @@ -178,21 +133,14 @@ function createTasksStore() { })); try { const updated = await tasksAPI.stop(uuid); - update(tasks => tasks.map(t => - t.uuid === uuid ? updated : t - )); + updateByUuid(uuid, () => updated); } catch (error) { - update(tasks => tasks.map(t => - t.uuid === uuid ? { ...t, start: prevStart } : t - )); + updateByUuid(uuid, t => ({ ...t, start: prevStart })); throw error; } }, - /** - * Complete task - * @param {string} uuid - */ + /** @param {string} uuid */ async complete(uuid) { try { await tasksAPI.complete(uuid); @@ -211,7 +159,6 @@ function createTasksStore() { export const tasksStore = createTasksStore(); -// Derived stores for filtered views export const pendingTasks = derived( tasksStore, $tasks => $tasks.filter(t => t.status === 'P') diff --git a/opal-web/src/lib/stores/theme.js b/opal-web/src/lib/stores/theme.js index f8bf333..ffaf5a6 100644 --- a/opal-web/src/lib/stores/theme.js +++ b/opal-web/src/lib/stores/theme.js @@ -11,10 +11,7 @@ const DEFAULT_THEME = 'obsidian'; /** @type {ThemeName[]} */ export const THEMES = ['obsidian', 'paper', 'midnight']; -/** - * Read stored theme, falling back to default - * @returns {ThemeName} - */ +/** @returns {ThemeName} */ function getInitial() { if (!browser) return DEFAULT_THEME; const stored = localStorage.getItem(STORAGE_KEY); @@ -27,7 +24,6 @@ function getInitial() { function createThemeStore() { const { subscribe, set, update } = writable(getInitial()); - /** Apply theme to the document */ function apply(/** @type {ThemeName} */ theme) { if (browser) { document.documentElement.dataset.theme = theme; @@ -35,7 +31,6 @@ function createThemeStore() { } } - // Apply on every change subscribe(apply); return { @@ -44,7 +39,6 @@ function createThemeStore() { set(theme) { set(theme); }, - /** Cycle to the next theme */ cycle() { update(current => { const idx = THEMES.indexOf(current); diff --git a/opal-web/src/lib/utils/dates.js b/opal-web/src/lib/utils/dates.js index 44d0270..9c321b2 100644 --- a/opal-web/src/lib/utils/dates.js +++ b/opal-web/src/lib/utils/dates.js @@ -1,7 +1,6 @@ import { format, formatDistance, isToday, isTomorrow, isPast } from 'date-fns'; /** - * Format Unix timestamp to readable date * @param {number|null} timestamp - Unix timestamp (seconds) * @param {string} formatStr - date-fns format string * @returns {string} @@ -12,23 +11,21 @@ export function formatDate(timestamp, formatStr = 'MMM d, yyyy') { } /** - * Format Unix timestamp to relative time - * @param {number|null} timestamp + * @param {number|null} timestamp - Unix timestamp (seconds) * @returns {string} */ export function formatRelative(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp * 1000); - + if (isToday(date)) return 'Today'; if (isTomorrow(date)) return 'Tomorrow'; - + return formatDistance(date, new Date(), { addSuffix: true }); } /** - * Check if timestamp is overdue - * @param {number|null} timestamp + * @param {number|null} timestamp - Unix timestamp (seconds) * @returns {boolean} */ export function isOverdue(timestamp) { @@ -36,20 +33,12 @@ export function isOverdue(timestamp) { return isPast(new Date(timestamp * 1000)); } -/** - * Convert Date object to Unix timestamp - * @param {Date} date - * @returns {number} - */ +/** @param {Date} date @returns {number} */ export function toUnix(date) { return Math.floor(date.getTime() / 1000); } -/** - * Convert Unix timestamp to Date object - * @param {number} timestamp - * @returns {Date} - */ +/** @param {number} timestamp @returns {Date} */ export function fromUnix(timestamp) { return new Date(timestamp * 1000); } diff --git a/opal-web/src/lib/utils/filters.js b/opal-web/src/lib/utils/filters.js index 5c90b0c..7998f21 100644 --- a/opal-web/src/lib/utils/filters.js +++ b/opal-web/src/lib/utils/filters.js @@ -1,6 +1,4 @@ -/** - * Valid filter attribute keys (these actually work as query filters in the engine) - */ +/** Valid filter attribute keys that work as query filters in the engine */ const FILTER_ATTRS = new Set(['status', 'project', 'priority']); /** @@ -13,7 +11,6 @@ const FILTER_ATTRS = new Set(['status', 'project', 'priority']); /** * Parse a filter string into structured tokens. * Recognizes +tag, -tag, and key:value for supported filter attributes. - * Unknown tokens (like due:3d) are preserved as raw tokens for pass-through. * @param {string} str * @returns {ParsedFilter} */ @@ -41,7 +38,6 @@ export function parseFilterString(str) { } /** - * Convert a parsed filter to TaskFilters for the API * @param {ParsedFilter} parsed * @returns {import('$lib/api/types.js').TaskFilters} */ @@ -58,8 +54,7 @@ export function filterToParams(parsed) { /** * Remove a token matching a given prefix from a string. - * Used by PropertyPills smart replace: e.g. removeTokenByPrefix("buy milk due:tomorrow", "due:") - * returns "buy milk" + * e.g. removeTokenByPrefix("buy milk due:tomorrow", "due:") → "buy milk" * @param {string} input * @param {string} prefix * @returns {string} @@ -72,7 +67,6 @@ export function removeTokenByPrefix(input, prefix) { /** * Deduplicate filter tokens from user input that are already in the active filter. - * Prevents submitting "+grocer +grocer" when filter is +grocer and user also typed +grocer. * @param {string} userInput * @param {string} filterStr * @returns {string} diff --git a/opal-web/src/lib/utils/storage.js b/opal-web/src/lib/utils/storage.js index d31900c..df6cd24 100644 --- a/opal-web/src/lib/utils/storage.js +++ b/opal-web/src/lib/utils/storage.js @@ -1,13 +1,12 @@ import { browser } from '$app/environment'; /** - * Get item from localStorage * @param {string} key * @returns {any} */ export function getItem(key) { if (!browser) return null; - + try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : null; @@ -18,13 +17,12 @@ export function getItem(key) { } /** - * Set item in localStorage * @param {string} key * @param {any} value */ export function setItem(key, value) { if (!browser) return; - + try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { @@ -32,13 +30,10 @@ export function setItem(key, value) { } } -/** - * Remove item from localStorage - * @param {string} key - */ +/** @param {string} key */ export function removeItem(key) { if (!browser) return; - + try { localStorage.removeItem(key); } catch (error) { @@ -46,12 +41,9 @@ export function removeItem(key) { } } -/** - * Clear all items - */ export function clear() { if (!browser) return; - + try { localStorage.clear(); } catch (error) { diff --git a/opal-web/src/lib/utils/sync-queue.js b/opal-web/src/lib/utils/sync-queue.js index 3f1c6ba..be8f848 100644 --- a/opal-web/src/lib/utils/sync-queue.js +++ b/opal-web/src/lib/utils/sync-queue.js @@ -1,55 +1,33 @@ import { getItem, setItem } from './storage.js'; import { generateUUID } from './uuid.js'; -/** - * @typedef {import('$lib/api/types.js').QueuedChange} QueuedChange - */ +/** @typedef {import('$lib/api/types.js').QueuedChange} QueuedChange */ const QUEUE_KEY = 'opal_sync_queue'; -/** - * Add change to sync queue - * @param {Omit} change - */ +/** @param {Omit} change */ export function queueChange(change) { const queue = getQueue(); - + queue.push({ id: generateUUID(), timestamp: Math.floor(Date.now() / 1000), ...change }); - + setItem(QUEUE_KEY, queue); } -/** - * Get all queued changes - * @returns {QueuedChange[]} - */ +/** @returns {QueuedChange[]} */ export function getQueue() { return getItem(QUEUE_KEY) || []; } -/** - * Clear sync queue - */ export function clearQueue() { setItem(QUEUE_KEY, []); } -/** - * Get queue size - * @returns {number} - */ -export function getQueueSize() { - return getQueue().length; -} - -/** - * Remove specific change from queue - * @param {string} id - */ +/** @param {string} id */ export function removeFromQueue(id) { const queue = getQueue().filter((change) => change.id !== id); setItem(QUEUE_KEY, queue); diff --git a/opal-web/src/lib/utils/uuid.js b/opal-web/src/lib/utils/uuid.js index aad1166..3e99c98 100644 --- a/opal-web/src/lib/utils/uuid.js +++ b/opal-web/src/lib/utils/uuid.js @@ -1,8 +1,8 @@ -/** - * Generate UUID v4 - * @returns {string} - */ +/** @returns {string} */ export function generateUUID() { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; diff --git a/opal-web/src/routes/settings/+page.svelte b/opal-web/src/routes/settings/+page.svelte index 25c679f..e87c6fc 100644 --- a/opal-web/src/routes/settings/+page.svelte +++ b/opal-web/src/routes/settings/+page.svelte @@ -11,9 +11,6 @@ let saving = false; let error = ''; - /** - * Save API key as manual auth - */ async function saveApiKey() { if (!apiKey.trim()) { error = 'API key is required'; @@ -24,7 +21,6 @@ error = ''; try { - // Store API key as access token (for manual auth mode) authStore.setAuth({ access_token: apiKey, refresh_token: '', @@ -45,9 +41,6 @@ } } - /** - * Logout - */ async function logout() { if ($authStore.refreshToken) { try { @@ -61,9 +54,6 @@ goto('/auth/login'); } - /** - * Trigger manual sync - */ async function triggerSync() { try { await syncStore.sync();