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:
2026-02-14 17:25:17 +01:00
parent 7c97440366
commit 83a9689e47
2 changed files with 195 additions and 10 deletions
+22
View File
@@ -98,6 +98,28 @@ export const tasks = {
*/
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<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;
}
};
+173 -10
View File
@@ -1,26 +1,142 @@
import { writable, derived } from 'svelte/store';
import { tasks as tasksAPI } from '$lib/api/endpoints.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').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
*/
function createTasksStore() {
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 {
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]
*/
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 {
const tasks = await tasksAPI.list(filters);
set(tasks);
@@ -29,12 +145,19 @@ function createTasksStore() {
throw error;
}
},
/**
* Add new task (optimistic update)
* @param {Partial<Task>} 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 {
const created = await tasksAPI.create(task);
update(tasks => [...tasks, created]);
@@ -46,13 +169,13 @@ function createTasksStore() {
task_uuid: task.uuid,
data: task
});
// Still update UI optimistically
update(tasks => [...tasks, task]);
throw error;
}
},
/**
* Update task (optimistic update)
* @param {string} uuid
@@ -67,7 +190,15 @@ function createTasksStore() {
}
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 {
const updated = await tasksAPI.update(uuid, updates);
// Sync with server response
@@ -88,7 +219,7 @@ function createTasksStore() {
throw error;
}
},
/**
* Delete task
* @param {string} uuid
@@ -96,7 +227,12 @@ function createTasksStore() {
async deleteTask(uuid) {
// Optimistic removal
update(tasks => tasks.filter(t => t.uuid !== uuid));
if (MOCK_MODE) {
mockData = mockData.filter(t => t.uuid !== uuid);
return;
}
try {
await tasksAPI.delete(uuid);
} catch (error) {
@@ -108,12 +244,39 @@ function createTasksStore() {
throw error;
}
},
/**
* Complete task
* @param {string} 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 {
const completed = await tasksAPI.complete(uuid);
update(tasks => {