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:
@@ -9,11 +9,6 @@
|
||||
*/
|
||||
export let onSelect;
|
||||
|
||||
/**
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
export let anchorEl;
|
||||
|
||||
/** @type {HTMLElement|null} */
|
||||
let popoverEl = null;
|
||||
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
];
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user