feat(web): add parse and report API endpoints with store methods
Add tasks.parse() and tasks.listByReport() to the API layer, and loadReport() and parseAndCreate() to the tasks store with mock mode support for the CLI-passthrough redesign. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,28 @@ export const tasks = {
|
|||||||
*/
|
*/
|
||||||
async stop(uuid) {
|
async stop(uuid) {
|
||||||
return apiRequest(`/tasks/${uuid}/stop`, { method: 'POST' });
|
return apiRequest(`/tasks/${uuid}/stop`, { method: 'POST' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse CLI input and create task
|
||||||
|
* @param {string} input - Raw opal CLI syntax
|
||||||
|
* @returns {Promise<Task>}
|
||||||
|
*/
|
||||||
|
async parse(input) {
|
||||||
|
return apiRequest('/tasks/parse', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ input })
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List tasks by report name
|
||||||
|
* @param {string} reportName
|
||||||
|
* @returns {Promise<Task[]>}
|
||||||
|
*/
|
||||||
|
async listByReport(reportName) {
|
||||||
|
const result = await apiRequest(`/tasks?report=${encodeURIComponent(reportName)}`);
|
||||||
|
return result.tasks ?? result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,142 @@
|
|||||||
import { writable, derived } from 'svelte/store';
|
import { writable, derived } from 'svelte/store';
|
||||||
import { tasks as tasksAPI } from '$lib/api/endpoints.js';
|
import { tasks as tasksAPI } from '$lib/api/endpoints.js';
|
||||||
import { queueChange } from '$lib/utils/sync-queue.js';
|
import { queueChange } from '$lib/utils/sync-queue.js';
|
||||||
|
import { generateUUID } from '$lib/utils/uuid.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('$lib/api/types.js').Task} Task
|
* @typedef {import('$lib/api/types.js').Task} Task
|
||||||
* @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters
|
* @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const MOCK_MODE = import.meta.env.VITE_MOCK_MODE === 'true';
|
||||||
|
|
||||||
|
/** Report names that map to pending tasks in mock mode */
|
||||||
|
const PENDING_REPORTS = new Set(['list', 'next', 'active', 'ready', 'overdue', 'waiting', 'newest', 'oldest']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create tasks store
|
* Create tasks store
|
||||||
*/
|
*/
|
||||||
function createTasksStore() {
|
function createTasksStore() {
|
||||||
const { subscribe, set, update } = writable(/** @type {Task[]} */ ([]));
|
const { subscribe, set, update } = writable(/** @type {Task[]} */ ([]));
|
||||||
|
|
||||||
|
/** @type {Task[]} */
|
||||||
|
let mockData = [];
|
||||||
|
|
||||||
|
/** Ensure mock data is loaded */
|
||||||
|
async function ensureMockData() {
|
||||||
|
if (mockData.length === 0) {
|
||||||
|
const { mockTasks } = await import('$lib/mock/tasks.js');
|
||||||
|
mockData = [...mockTasks];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all tasks from API
|
* Load tasks by report name
|
||||||
|
* @param {string} reportName - Backend report name (e.g. 'list', 'next', 'completed')
|
||||||
|
*/
|
||||||
|
async loadReport(reportName) {
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
await ensureMockData();
|
||||||
|
if (reportName === 'completed') {
|
||||||
|
set(mockData.filter(t => t.status === 'C'));
|
||||||
|
} else if (PENDING_REPORTS.has(reportName)) {
|
||||||
|
set(mockData.filter(t => t.status === 'P'));
|
||||||
|
} else {
|
||||||
|
set(mockData.filter(t => t.status === 'P'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tasks = await tasksAPI.listByReport(reportName);
|
||||||
|
set(tasks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load report:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse CLI input and create a task
|
||||||
|
* @param {string} input - Raw opal CLI syntax
|
||||||
|
* @returns {Promise<Task>}
|
||||||
|
*/
|
||||||
|
async parseAndCreate(input) {
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
await ensureMockData();
|
||||||
|
// Naive parse: non-modifier words become description
|
||||||
|
const words = input.split(/\s+/);
|
||||||
|
const descWords = [];
|
||||||
|
const task = /** @type {Task} */ ({
|
||||||
|
uuid: generateUUID(),
|
||||||
|
id: mockData.length + 1,
|
||||||
|
status: 'P',
|
||||||
|
description: '',
|
||||||
|
project: null,
|
||||||
|
priority: 1,
|
||||||
|
created: Date.now() / 1000,
|
||||||
|
modified: Date.now() / 1000,
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
due: null,
|
||||||
|
scheduled: null,
|
||||||
|
wait: null,
|
||||||
|
until: null,
|
||||||
|
recurrence_duration: null,
|
||||||
|
parent_uuid: null,
|
||||||
|
tags: []
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
const w = words[i];
|
||||||
|
if (w.startsWith('project:')) {
|
||||||
|
task.project = w.slice(8);
|
||||||
|
} else if (w.startsWith('priority:') || w.startsWith('pri:')) {
|
||||||
|
const val = w.includes(':') ? w.split(':')[1] : '1';
|
||||||
|
/** @type {Record<string, number>} */
|
||||||
|
const map = { H: 3, M: 2, L: 0, h: 3, m: 2, l: 0 };
|
||||||
|
task.priority = /** @type {import('$lib/api/types.js').TaskPriority} */ ((map[val] ?? parseInt(val, 10)) || 1);
|
||||||
|
} else if (w.startsWith('+')) {
|
||||||
|
task.tags = [...(task.tags || []), w.slice(1)];
|
||||||
|
} else {
|
||||||
|
descWords.push(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.description = descWords.join(' ') || 'New task';
|
||||||
|
|
||||||
|
mockData = [task, ...mockData];
|
||||||
|
update(tasks => [task, ...tasks]);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await tasksAPI.parse(input);
|
||||||
|
update(tasks => [created, ...tasks]);
|
||||||
|
return created;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse and create task:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all tasks from API (or mock data in dev)
|
||||||
* @param {TaskFilters} [filters]
|
* @param {TaskFilters} [filters]
|
||||||
*/
|
*/
|
||||||
async load(filters = {}) {
|
async load(filters = {}) {
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
await ensureMockData();
|
||||||
|
let filtered = mockData;
|
||||||
|
if (filters.status) {
|
||||||
|
filtered = filtered.filter(t => t.status === filters.status);
|
||||||
|
}
|
||||||
|
set(filtered);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tasks = await tasksAPI.list(filters);
|
const tasks = await tasksAPI.list(filters);
|
||||||
set(tasks);
|
set(tasks);
|
||||||
@@ -29,12 +145,19 @@ function createTasksStore() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new task (optimistic update)
|
* Add new task (optimistic update)
|
||||||
* @param {Partial<Task>} task
|
* @param {Partial<Task>} task
|
||||||
*/
|
*/
|
||||||
async add(task) {
|
async add(task) {
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
const fullTask = /** @type {Task} */ ({ ...task, id: mockData.length + 1 });
|
||||||
|
mockData = [...mockData, fullTask];
|
||||||
|
update(tasks => [...tasks, fullTask]);
|
||||||
|
return fullTask;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await tasksAPI.create(task);
|
const created = await tasksAPI.create(task);
|
||||||
update(tasks => [...tasks, created]);
|
update(tasks => [...tasks, created]);
|
||||||
@@ -46,13 +169,13 @@ function createTasksStore() {
|
|||||||
task_uuid: task.uuid,
|
task_uuid: task.uuid,
|
||||||
data: task
|
data: task
|
||||||
});
|
});
|
||||||
|
|
||||||
// Still update UI optimistically
|
// Still update UI optimistically
|
||||||
update(tasks => [...tasks, task]);
|
update(tasks => [...tasks, task]);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update task (optimistic update)
|
* Update task (optimistic update)
|
||||||
* @param {string} uuid
|
* @param {string} uuid
|
||||||
@@ -67,7 +190,15 @@ function createTasksStore() {
|
|||||||
}
|
}
|
||||||
return tasks;
|
return tasks;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
const index = mockData.findIndex(t => t.uuid === uuid);
|
||||||
|
if (index >= 0) {
|
||||||
|
mockData[index] = { ...mockData[index], ...updates, modified: Date.now() / 1000 };
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await tasksAPI.update(uuid, updates);
|
const updated = await tasksAPI.update(uuid, updates);
|
||||||
// Sync with server response
|
// Sync with server response
|
||||||
@@ -88,7 +219,7 @@ function createTasksStore() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete task
|
* Delete task
|
||||||
* @param {string} uuid
|
* @param {string} uuid
|
||||||
@@ -96,7 +227,12 @@ function createTasksStore() {
|
|||||||
async deleteTask(uuid) {
|
async deleteTask(uuid) {
|
||||||
// Optimistic removal
|
// Optimistic removal
|
||||||
update(tasks => tasks.filter(t => t.uuid !== uuid));
|
update(tasks => tasks.filter(t => t.uuid !== uuid));
|
||||||
|
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
mockData = mockData.filter(t => t.uuid !== uuid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tasksAPI.delete(uuid);
|
await tasksAPI.delete(uuid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -108,12 +244,39 @@ function createTasksStore() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete task
|
* Complete task
|
||||||
* @param {string} uuid
|
* @param {string} uuid
|
||||||
*/
|
*/
|
||||||
async complete(uuid) {
|
async complete(uuid) {
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
update(tasks => {
|
||||||
|
const index = tasks.findIndex(t => t.uuid === uuid);
|
||||||
|
if (index >= 0) {
|
||||||
|
const newStatus = tasks[index].status === 'C' ? 'P' : 'C';
|
||||||
|
tasks[index] = {
|
||||||
|
...tasks[index],
|
||||||
|
status: /** @type {'P'|'C'} */ (newStatus),
|
||||||
|
end: newStatus === 'C' ? Date.now() / 1000 : null,
|
||||||
|
modified: Date.now() / 1000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return [...tasks];
|
||||||
|
});
|
||||||
|
const mi = mockData.findIndex(t => t.uuid === uuid);
|
||||||
|
if (mi >= 0) {
|
||||||
|
const newStatus = mockData[mi].status === 'C' ? 'P' : 'C';
|
||||||
|
mockData[mi] = {
|
||||||
|
...mockData[mi],
|
||||||
|
status: /** @type {'P'|'C'} */ (newStatus),
|
||||||
|
end: newStatus === 'C' ? Date.now() / 1000 : null,
|
||||||
|
modified: Date.now() / 1000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const completed = await tasksAPI.complete(uuid);
|
const completed = await tasksAPI.complete(uuid);
|
||||||
update(tasks => {
|
update(tasks => {
|
||||||
|
|||||||
Reference in New Issue
Block a user