feat: add parse endpoint, refactor recurring tasks, and improve web task completion

Extract CreateRecurringTask into engine package for reuse by both CLI
and API. Add POST /tasks/parse endpoint for CLI-style input parsing.
Remove FK constraint on change_log to preserve history after task
deletion. Update web frontend to filter completed tasks from view and
add mock mode support for development.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 22:39:11 +01:00
parent 0352c22b4f
commit 78881e1b07
15 changed files with 2118 additions and 128 deletions
@@ -9,11 +9,6 @@
*/
export let onSelect;
/**
* @type {HTMLElement}
*/
export let anchorEl;
/** @type {HTMLElement|null} */
let popoverEl = null;
+300
View File
@@ -0,0 +1,300 @@
/**
* Mock task data for local development / design review.
* Covers a realistic spread of projects, priorities, tags, due dates, and statuses.
*/
const now = Math.floor(Date.now() / 1000);
const HOUR = 3600;
const DAY = 86400;
/** @type {import('$lib/api/types.js').Task[]} */
export const mockTasks = [
// ── Pending tasks ────────────────────────────────────────────
{
uuid: '11111111-1111-4111-a111-111111111101',
id: 1,
status: 'P',
description: 'Set up Caddy reverse proxy for opal-web',
project: 'Infrastructure',
priority: 3,
created: now - 7 * DAY,
modified: now - 1 * DAY,
start: now - 2 * HOUR,
end: null,
due: now + 2 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['devops', 'selfhosted']
},
{
uuid: '11111111-1111-4111-a111-111111111102',
id: 2,
status: 'P',
description: 'Write unit tests for task filter parsing',
project: 'Opal',
priority: 2,
created: now - 5 * DAY,
modified: now - 3 * DAY,
start: null,
end: null,
due: now + 5 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['testing', 'backend']
},
{
uuid: '11111111-1111-4111-a111-111111111103',
id: 3,
status: 'P',
description: 'Fix tag extraction for nested wiki-links',
project: 'Jade',
priority: 2,
created: now - 3 * DAY,
modified: now - 3 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['bug']
},
{
uuid: '11111111-1111-4111-a111-111111111104',
id: 4,
status: 'P',
description: 'Grocery run - farmers market',
project: null,
priority: 1,
created: now - 1 * DAY,
modified: now - 1 * DAY,
start: null,
end: null,
due: now + 1 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['errand']
},
{
uuid: '11111111-1111-4111-a111-111111111105',
id: 5,
status: 'P',
description: 'Design task detail page for opal-web',
project: 'Opal',
priority: 3,
created: now - 2 * DAY,
modified: now - 2 * DAY,
start: null,
end: null,
due: now - 1 * DAY, // overdue
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['frontend', 'design']
},
{
uuid: '11111111-1111-4111-a111-111111111106',
id: 6,
status: 'P',
description: 'Renew domain registration for jnss.me',
project: 'Infrastructure',
priority: 1,
created: now - 14 * DAY,
modified: now - 14 * DAY,
start: null,
end: null,
due: now + 30 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['admin']
},
{
uuid: '11111111-1111-4111-a111-111111111107',
id: 7,
status: 'P',
description: 'Add recurrence UI to task creation form',
project: 'Opal',
priority: 1,
created: now - 4 * DAY,
modified: now - 4 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['frontend']
},
{
uuid: '11111111-1111-4111-a111-111111111108',
id: 8,
status: 'P',
description: 'Migrate Nextcloud to latest stable',
project: 'Infrastructure',
priority: 0,
created: now - 10 * DAY,
modified: now - 10 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['selfhosted', 'maintenance']
},
{
uuid: '11111111-1111-4111-a111-111111111109',
id: 9,
status: 'P',
description: 'Read "Designing Data-Intensive Applications" ch. 7',
project: null,
priority: 0,
created: now - 6 * DAY,
modified: now - 6 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['reading', 'learning']
},
{
uuid: '11111111-1111-4111-a111-111111111110',
id: 10,
status: 'P',
description: 'Review PR: sync conflict resolution strategy',
project: 'Opal',
priority: 2,
created: now - 1 * DAY,
modified: now - 1 * DAY,
start: null,
end: null,
due: now, // due today
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['review', 'backend']
},
// ── Completed tasks ──────────────────────────────────────────
{
uuid: '22222222-2222-4222-a222-222222222201',
id: 11,
status: 'C',
description: 'Implement XDG directory support',
project: 'Opal',
priority: 2,
created: now - 14 * DAY,
modified: now - 7 * DAY,
start: null,
end: now - 7 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['backend', 'refactor']
},
{
uuid: '22222222-2222-4222-a222-222222222202',
id: 12,
status: 'C',
description: 'Set up Authentik OAuth provider',
project: 'Infrastructure',
priority: 3,
created: now - 21 * DAY,
modified: now - 10 * DAY,
start: null,
end: now - 10 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['auth', 'selfhosted']
},
{
uuid: '22222222-2222-4222-a222-222222222203',
id: 13,
status: 'C',
description: 'Build setup wizard for first-run',
project: 'Opal',
priority: 2,
created: now - 10 * DAY,
modified: now - 5 * DAY,
start: null,
end: now - 5 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['ux', 'backend']
},
{
uuid: '22222222-2222-4222-a222-222222222204',
id: 14,
status: 'C',
description: 'Fix PersistentPreRun initialization order',
project: 'Opal',
priority: 3,
created: now - 8 * DAY,
modified: now - 6 * DAY,
start: null,
end: now - 6 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['bug', 'backend']
},
{
uuid: '22222222-2222-4222-a222-222222222205',
id: 15,
status: 'C',
description: 'Write deployment guide with Caddy config',
project: 'Opal',
priority: 1,
created: now - 9 * DAY,
modified: now - 6 * DAY,
start: null,
end: now - 6 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['docs']
}
];
+14 -2
View File
@@ -17,12 +17,24 @@ import { getItem, setItem, removeItem } from '$lib/utils/storage.js';
*/
const STORAGE_KEY = 'opal_auth';
const MOCK_MODE = import.meta.env.VITE_MOCK_MODE === 'true';
/**
* Load auth state from localStorage
* @returns {AuthState}
*/
function loadAuth() {
// In mock mode, always return authenticated
if (MOCK_MODE) {
return {
accessToken: 'mock-token',
refreshToken: '',
expiresAt: 9999999999,
user: { id: 1, username: 'dev', email: 'dev@localhost' },
isAuthenticated: true
};
}
if (!browser) {
return {
accessToken: null,
@@ -32,7 +44,7 @@ function loadAuth() {
isAuthenticated: false
};
}
const stored = getItem(STORAGE_KEY);
if (stored) {
// Check if token expired
@@ -46,7 +58,7 @@ function loadAuth() {
isAuthenticated: Boolean(stored.accessToken)
};
}
return {
accessToken: null,
refreshToken: null,
+5 -24
View File
@@ -251,41 +251,22 @@ function createTasksStore() {
*/
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,
status: /** @type {'P'|'C'} */ ('C'),
end: Date.now() / 1000,
modified: Date.now() / 1000
};
}
update(tasks => tasks.filter(t => t.uuid !== uuid));
return;
}
try {
const completed = await tasksAPI.complete(uuid);
update(tasks => {
const index = tasks.findIndex(t => t.uuid === uuid);
if (index >= 0) {
tasks[index] = completed;
}
return tasks;
});
await tasksAPI.complete(uuid);
update(tasks => tasks.filter(t => t.uuid !== uuid));
} catch (error) {
queueChange({
type: 'update',