@plandesk/api 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/events.d.ts +20 -1
  2. package/dist/index.d.ts +8 -1
  3. package/dist/index.js +5 -1
  4. package/dist/routes/agent-runs.d.ts +1 -1
  5. package/dist/routes/agent-runs.js +20 -0
  6. package/dist/routes/documents.js +2 -0
  7. package/dist/routes/folders.d.ts +4 -0
  8. package/dist/routes/folders.js +69 -0
  9. package/dist/routes/goals.d.ts +4 -0
  10. package/dist/routes/goals.js +138 -0
  11. package/dist/routes/projects.d.ts +1 -1
  12. package/dist/routes/projects.js +23 -3
  13. package/dist/routes/submissions.d.ts +5 -0
  14. package/dist/routes/submissions.js +58 -0
  15. package/dist/routes/tags.d.ts +4 -0
  16. package/dist/routes/tags.js +62 -0
  17. package/dist/routes/tasks.d.ts +1 -0
  18. package/dist/routes/tasks.js +9 -1
  19. package/dist/serialize.d.ts +51 -2
  20. package/dist/serialize.js +101 -1
  21. package/dist/server.js +9 -1
  22. package/dist/services/canvas.d.ts +4 -0
  23. package/dist/services/canvas.js +3 -2
  24. package/dist/services/documents.d.ts +5 -1
  25. package/dist/services/documents.js +31 -2
  26. package/dist/services/folders.d.ts +27 -0
  27. package/dist/services/folders.js +107 -0
  28. package/dist/services/goals.d.ts +239 -0
  29. package/dist/services/goals.js +323 -0
  30. package/dist/services/index.d.ts +6 -0
  31. package/dist/services/index.js +9 -0
  32. package/dist/services/projects.js +8 -2
  33. package/dist/services/sync.d.ts +5 -3
  34. package/dist/services/sync.js +45 -2
  35. package/dist/services/tags.d.ts +27 -0
  36. package/dist/services/tags.js +79 -0
  37. package/dist/services/tasks.d.ts +20 -2
  38. package/dist/services/tasks.js +98 -26
  39. package/package.json +3 -3
  40. package/web/assets/index-DcD7-VGi.css +1 -0
  41. package/web/assets/index-Dcaq3wR-.js +318 -0
  42. package/web/index.html +2 -2
  43. package/web/assets/index-CUxSE_WZ.css +0 -1
  44. package/web/assets/index-cyyf9-Uw.js +0 -234
@@ -1,4 +1,4 @@
1
- import { type Db, type ShareSubmission, type ShareSubmissionStatus, type TaskStatus } from '@plandesk/db';
1
+ import { type Db, type ShareSubmission, type ShareSubmissionStatus } from '@plandesk/db';
2
2
  import type { EventBus } from '../events.js';
3
3
  import type { ShareService } from './share.js';
4
4
  import type { TaskService } from './tasks.js';
@@ -11,6 +11,9 @@ export declare class SyncUnauthorizedError extends Error {
11
11
  export declare class InvalidTriageError extends Error {
12
12
  constructor(message?: string);
13
13
  }
14
+ export declare class InvalidTriageInputError extends Error {
15
+ constructor(message: string);
16
+ }
14
17
  export type SyncRemote = {
15
18
  serverUrl: string;
16
19
  globalProjectId: string;
@@ -65,9 +68,8 @@ export declare function createSyncService(deps: SyncServiceDeps): {
65
68
  getRemote(projectId: string): SyncRemote | undefined;
66
69
  triage(submissionId: string, action: "accept" | "reject", remote: SyncRemote, asTask?: {
67
70
  label?: string;
68
- status?: TaskStatus;
69
71
  description?: string;
70
- }): Promise<SerializedSubmission>;
72
+ }, linkTaskId?: string): Promise<SerializedSubmission>;
71
73
  watchPush(projectId: string, remote: SyncRemote, opts?: WatchPushOptions): WatchPushController;
72
74
  };
73
75
  export type SyncService = ReturnType<typeof createSyncService>;
@@ -18,6 +18,12 @@ export class InvalidTriageError extends Error {
18
18
  this.name = 'InvalidTriageError';
19
19
  }
20
20
  }
21
+ export class InvalidTriageInputError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'InvalidTriageInputError';
25
+ }
26
+ }
21
27
  export function serializeSubmission(row) {
22
28
  return {
23
29
  id: row.id,
@@ -252,19 +258,56 @@ export function createSyncService(deps) {
252
258
  syncToken: row.syncToken,
253
259
  };
254
260
  },
255
- async triage(submissionId, action, remote, asTask) {
261
+ async triage(submissionId, action, remote, asTask, linkTaskId) {
262
+ if (asTask !== undefined && linkTaskId !== undefined) {
263
+ throw new InvalidTriageInputError('as_task and link_task_id are mutually exclusive');
264
+ }
256
265
  const submission = dbGetSubmission(db, submissionId);
257
266
  if (submission === undefined) {
258
267
  throw new InvalidTriageError();
259
268
  }
260
269
  if (submission.status !== 'pending') {
270
+ // Idempotent retry recovery: a prior triage may have committed the terminal
271
+ // status locally but failed to ack the remote (sync server briefly down),
272
+ // leaving local/remote divergence with no recovery path. Re-ack when the
273
+ // retry's action matches the recorded outcome; ack is idempotent remotely.
274
+ if ((action === 'accept' && submission.status === 'accepted') ||
275
+ (action === 'reject' && submission.status === 'rejected')) {
276
+ await ackSubmission(remote, submissionId, submission.status);
277
+ }
261
278
  return serializeSubmission(submission);
262
279
  }
263
280
  const projectId = submission.projectId;
281
+ if (action === 'accept' && linkTaskId !== undefined) {
282
+ const existingTask = taskService.get(linkTaskId);
283
+ if (existingTask === undefined || existingTask.project_id !== projectId) {
284
+ throw new InvalidTriageInputError('link_task_id does not reference an existing task in this project');
285
+ }
286
+ const updated = setSubmissionStatus(db, submissionId, {
287
+ status: 'accepted',
288
+ linkedTaskId: linkTaskId,
289
+ });
290
+ if (updated === undefined) {
291
+ throw new InvalidTriageError();
292
+ }
293
+ eventBus.emit({ type: 'submissions_pulled', projectId });
294
+ await ackSubmission(remote, submissionId, 'accepted');
295
+ return serializeSubmission(updated);
296
+ }
264
297
  if (action === 'accept') {
298
+ // Triage never releases work to `todo`: the scope->todo gate is the human's
299
+ // own board action. Every accepted submission lands in `scope`, regardless of
300
+ // caller — enforced here at the single service chokepoint so both the HTTP
301
+ // route and the MCP tool are covered.
302
+ //
303
+ // create + setSubmissionStatus run as two synchronous steps, but the submission
304
+ // was validated to exist immediately above with no intervening await, so under
305
+ // better-sqlite3's single-connection sync model the row cannot vanish and
306
+ // setSubmissionStatus cannot spuriously return undefined. taskService.create
307
+ // self-transacts, so an outer transaction here is neither possible nor needed.
265
308
  const task = taskService.create(projectId, {
266
309
  label: asTask?.label ?? submission.title,
267
- status: asTask?.status ?? 'todo',
310
+ status: 'scope',
268
311
  description: asTask?.description ?? buildDesc(submission),
269
312
  });
270
313
  if (task === undefined) {
@@ -0,0 +1,27 @@
1
+ import { type Db } from '@plandesk/db';
2
+ import type { EventBus } from '../events.js';
3
+ import { type SerializedTag } from '../serialize.js';
4
+ export type TagServiceDeps = {
5
+ db: Db;
6
+ eventBus: EventBus;
7
+ };
8
+ export type CreateTagInput = {
9
+ name: string;
10
+ color?: string | null;
11
+ };
12
+ export type UpdateTagInput = {
13
+ name?: string;
14
+ color?: string | null;
15
+ };
16
+ export declare class InvalidTagError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare function normalizeTagName(name: string): string;
20
+ export declare function createTagService(deps: TagServiceDeps): {
21
+ list(projectId: string): SerializedTag[] | undefined;
22
+ create(projectId: string, input: CreateTagInput): SerializedTag | undefined;
23
+ update(id: string, input: UpdateTagInput): SerializedTag | undefined;
24
+ delete(id: string): boolean;
25
+ };
26
+ export type TagService = ReturnType<typeof createTagService>;
27
+ //# sourceMappingURL=tags.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { createTag as dbCreateTag, deleteTag as dbDeleteTag, getProject, getTag as dbGetTag, getTagByName, listTags as dbListTags, updateTag as dbUpdateTag, } from '@plandesk/db';
2
+ import { serializeTag } from '../serialize.js';
3
+ export class InvalidTagError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'InvalidTagError';
7
+ }
8
+ }
9
+ export function normalizeTagName(name) {
10
+ const trimmed = name.trim();
11
+ if (trimmed === '') {
12
+ throw new InvalidTagError('Tag name must not be empty');
13
+ }
14
+ return trimmed;
15
+ }
16
+ export function createTagService(deps) {
17
+ const { db, eventBus } = deps;
18
+ return {
19
+ list(projectId) {
20
+ const project = getProject(db, projectId);
21
+ if (!project) {
22
+ return undefined;
23
+ }
24
+ return dbListTags(db, projectId).map(serializeTag);
25
+ },
26
+ create(projectId, input) {
27
+ const project = getProject(db, projectId);
28
+ if (!project) {
29
+ return undefined;
30
+ }
31
+ const name = normalizeTagName(input.name);
32
+ if (getTagByName(db, projectId, name)) {
33
+ throw new InvalidTagError(`Tag already exists: ${name}`);
34
+ }
35
+ const tag = dbCreateTag(db, { projectId, name, color: input.color });
36
+ eventBus.emit({ type: 'tag_updated', projectId });
37
+ return serializeTag(tag);
38
+ },
39
+ // Renaming propagates everywhere automatically: tasks reference the single
40
+ // tag row through the join table.
41
+ update(id, input) {
42
+ const existing = dbGetTag(db, id);
43
+ if (!existing) {
44
+ return undefined;
45
+ }
46
+ let name;
47
+ if (input.name !== undefined) {
48
+ name = normalizeTagName(input.name);
49
+ const conflict = getTagByName(db, existing.projectId, name);
50
+ if (conflict && conflict.id !== id) {
51
+ throw new InvalidTagError(`Tag already exists: ${name}`);
52
+ }
53
+ }
54
+ const tag = dbUpdateTag(db, id, {
55
+ ...(name !== undefined ? { name } : {}),
56
+ ...(input.color !== undefined ? { color: input.color } : {}),
57
+ });
58
+ if (!tag) {
59
+ return undefined;
60
+ }
61
+ eventBus.emit({ type: 'tag_updated', projectId: tag.projectId });
62
+ return serializeTag(tag);
63
+ },
64
+ // Deleting a tag removes it from all its tasks (cascade on the join table).
65
+ delete(id) {
66
+ const existing = dbGetTag(db, id);
67
+ if (!existing) {
68
+ return false;
69
+ }
70
+ const deleted = dbDeleteTag(db, id);
71
+ if (!deleted) {
72
+ return false;
73
+ }
74
+ eventBus.emit({ type: 'tag_updated', projectId: existing.projectId });
75
+ return true;
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=tags.js.map
@@ -2,7 +2,10 @@ import { type Db, type TaskStatus } from '@plandesk/db';
2
2
  import type { EventBus } from '../events.js';
3
3
  import { serializeTask, type PaginationParams } from '../serialize.js';
4
4
  type SerializedTask = ReturnType<typeof serializeTask>;
5
- export type NextActionableReason = 'ok' | 'no_tasks' | 'no_todo_tasks' | 'all_blocked';
5
+ export declare class InvalidGoalReferenceError extends Error {
6
+ constructor(goalId: string);
7
+ }
8
+ export type NextActionableReason = 'ok' | 'no_tasks' | 'no_todo_tasks' | 'all_blocked' | 'no_active_goal' | 'multiple_active_goals';
6
9
  export type NextActionableResult = {
7
10
  next_task: SerializedTask | null;
8
11
  reason: NextActionableReason;
@@ -23,6 +26,8 @@ export type CreateTaskInput = {
23
26
  y?: number;
24
27
  assignee?: string | null;
25
28
  dueDate?: Date | null;
29
+ goalId?: string;
30
+ tags?: string[];
26
31
  };
27
32
  export type UpdateTaskInput = {
28
33
  label?: string;
@@ -30,14 +35,18 @@ export type UpdateTaskInput = {
30
35
  description?: string | null;
31
36
  x?: number;
32
37
  y?: number;
38
+ tags?: string[];
33
39
  };
34
40
  export type ListTasksFilter = {
35
41
  status?: string;
42
+ tags?: string[];
36
43
  };
37
44
  export declare function createTaskService(deps: TaskServiceDeps): {
38
45
  get(id: string): {
46
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
39
47
  id: string;
40
48
  project_id: string;
49
+ goal_id: string;
41
50
  label: string;
42
51
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
43
52
  description: string | null;
@@ -49,8 +58,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
49
58
  updated_at: string;
50
59
  } | undefined;
51
60
  listByProject(projectId: string, filter?: ListTasksFilter, pagination?: PaginationParams): {
61
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
52
62
  id: string;
53
63
  project_id: string;
64
+ goal_id: string;
54
65
  label: string;
55
66
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
56
67
  description: string | null;
@@ -62,8 +73,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
62
73
  updated_at: string;
63
74
  }[] | undefined;
64
75
  create(projectId: string, input: CreateTaskInput): {
76
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
65
77
  id: string;
66
78
  project_id: string;
79
+ goal_id: string;
67
80
  label: string;
68
81
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
69
82
  description: string | null;
@@ -75,8 +88,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
75
88
  updated_at: string;
76
89
  } | undefined;
77
90
  update(id: string, input: UpdateTaskInput): {
91
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
78
92
  id: string;
79
93
  project_id: string;
94
+ goal_id: string;
80
95
  label: string;
81
96
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
82
97
  description: string | null;
@@ -88,7 +103,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
88
103
  updated_at: string;
89
104
  } | undefined;
90
105
  delete(id: string): boolean;
91
- nextActionable(projectId: string): NextActionableResult | undefined;
106
+ nextActionable(projectId: string, filter?: {
107
+ goalId?: string;
108
+ tags?: string[];
109
+ }): NextActionableResult | undefined;
92
110
  };
93
111
  export type TaskService = ReturnType<typeof createTaskService>;
94
112
  export {};
@@ -1,5 +1,12 @@
1
- import { createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, getProject, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTasks, nullDocumentsLinkedTask, updateTask, } from '@plandesk/db';
1
+ import { createTag, createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, deleteTaskTagsByTaskId, getOrCreateDefaultGoal, getProject, listGoals, getTagByName, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTagsByTaskForProject, listTagsForTask, listTasks, nullDocumentsLinkedTask, setTaskTags, taskIdsWithAnyTagName, updateTask, } from '@plandesk/db';
2
2
  import { serializeTask } from '../serialize.js';
3
+ import { normalizeTagName } from './tags.js';
4
+ export class InvalidGoalReferenceError extends Error {
5
+ constructor(goalId) {
6
+ super(`Goal ${goalId} does not exist in this project`);
7
+ this.name = 'InvalidGoalReferenceError';
8
+ }
9
+ }
3
10
  // depends_on: prerequisite = to, dependent = from. All other labels: prerequisite = from, dependent = to.
4
11
  function prerequisiteAndDependent(edge) {
5
12
  if (edge.fromTaskId === edge.toTaskId) {
@@ -10,6 +17,21 @@ function prerequisiteAndDependent(edge) {
10
17
  }
11
18
  return { prerequisite: edge.fromTaskId, dependent: edge.toTaskId };
12
19
  }
20
+ // Resolves tag names to ids, auto-creating tags that do not exist yet.
21
+ function resolveTagIdsByName(db, projectId, names) {
22
+ const seen = new Set();
23
+ const ids = [];
24
+ for (const raw of names) {
25
+ const name = normalizeTagName(raw);
26
+ if (seen.has(name)) {
27
+ continue;
28
+ }
29
+ seen.add(name);
30
+ const tag = getTagByName(db, projectId, name) ?? createTag(db, { projectId, name });
31
+ ids.push(tag.id);
32
+ }
33
+ return ids;
34
+ }
13
35
  export function createTaskService(deps) {
14
36
  const { db, eventBus } = deps;
15
37
  return {
@@ -18,7 +40,7 @@ export function createTaskService(deps) {
18
40
  if (!task) {
19
41
  return undefined;
20
42
  }
21
- return serializeTask(task);
43
+ return serializeTask(task, listTagsForTask(db, id));
22
44
  },
23
45
  listByProject(projectId, filter = {}, pagination = {}) {
24
46
  if (filter.status !== undefined && !isTaskStatus(filter.status)) {
@@ -29,10 +51,13 @@ export function createTaskService(deps) {
29
51
  return undefined;
30
52
  }
31
53
  const statusFilter = filter.status;
32
- const tasks = statusFilter !== undefined
33
- ? listTasks(db, projectId, { status: statusFilter, ...pagination })
34
- : listTasks(db, projectId, pagination);
35
- return tasks.map(serializeTask);
54
+ const tasks = listTasks(db, projectId, {
55
+ ...(statusFilter !== undefined ? { status: statusFilter } : {}),
56
+ ...(filter.tags !== undefined ? { tagNames: filter.tags.map(normalizeTagName) } : {}),
57
+ ...pagination,
58
+ });
59
+ const tagsByTask = listTagsByTaskForProject(db, projectId);
60
+ return tasks.map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
36
61
  },
37
62
  create(projectId, input) {
38
63
  if (input.status !== undefined && !isTaskStatus(input.status)) {
@@ -42,22 +67,34 @@ export function createTaskService(deps) {
42
67
  if (!project) {
43
68
  return undefined;
44
69
  }
45
- const task = createTask(db, {
46
- projectId,
47
- label: input.label,
48
- status: input.status,
49
- description: input.description,
50
- x: input.x,
51
- y: input.y,
52
- assignee: input.assignee,
53
- dueDate: input.dueDate,
70
+ if (input.goalId !== undefined &&
71
+ !listGoals(db, projectId).some((g) => g.id === input.goalId)) {
72
+ throw new InvalidGoalReferenceError(input.goalId);
73
+ }
74
+ const { task, tags } = db.transaction((tx) => {
75
+ const goalId = input.goalId ?? getOrCreateDefaultGoal(tx, projectId).id;
76
+ const row = createTask(tx, {
77
+ projectId,
78
+ goalId,
79
+ label: input.label,
80
+ status: input.status,
81
+ description: input.description,
82
+ x: input.x,
83
+ y: input.y,
84
+ assignee: input.assignee,
85
+ dueDate: input.dueDate,
86
+ });
87
+ if (input.tags !== undefined) {
88
+ setTaskTags(tx, row.id, resolveTagIdsByName(tx, projectId, input.tags));
89
+ }
90
+ return { task: row, tags: listTagsForTask(tx, row.id) };
54
91
  });
55
92
  eventBus.emit({
56
93
  type: 'task_updated',
57
94
  taskId: task.id,
58
95
  projectId,
59
96
  });
60
- return serializeTask(task);
97
+ return serializeTask(task, tags);
61
98
  },
62
99
  update(id, input) {
63
100
  if (input.status !== undefined && !isTaskStatus(input.status)) {
@@ -67,16 +104,26 @@ export function createTaskService(deps) {
67
104
  if (!existing) {
68
105
  return undefined;
69
106
  }
70
- const task = updateTask(db, id, input);
71
- if (!task) {
107
+ const { tags: tagNames, ...columns } = input;
108
+ const result = db.transaction((tx) => {
109
+ const row = updateTask(tx, id, columns);
110
+ if (!row) {
111
+ return undefined;
112
+ }
113
+ if (tagNames !== undefined) {
114
+ setTaskTags(tx, id, resolveTagIdsByName(tx, existing.projectId, tagNames));
115
+ }
116
+ return { task: row, tags: listTagsForTask(tx, id) };
117
+ });
118
+ if (!result) {
72
119
  return undefined;
73
120
  }
74
121
  eventBus.emit({
75
122
  type: 'task_updated',
76
- taskId: task.id,
77
- projectId: task.projectId,
123
+ taskId: result.task.id,
124
+ projectId: result.task.projectId,
78
125
  });
79
- return serializeTask(task);
126
+ return serializeTask(result.task, result.tags);
80
127
  },
81
128
  delete(id) {
82
129
  const task = getTask(db, id);
@@ -87,19 +134,42 @@ export function createTaskService(deps) {
87
134
  db.transaction((tx) => {
88
135
  deleteEdgesByTaskId(tx, id);
89
136
  nullDocumentsLinkedTask(tx, id);
137
+ deleteTaskTagsByTaskId(tx, id);
90
138
  dbDeleteTask(tx, id);
91
139
  });
92
140
  eventBus.emit({ type: 'canvas_updated', projectId });
93
141
  return true;
94
142
  },
95
- nextActionable(projectId) {
143
+ // filter.goalId scopes candidates to one goal; when omitted, the project's sole
144
+ // active goal is resolved. filter.tags (OR semantics) composes with goal scope;
145
+ // prerequisite completion is still evaluated against all tasks in the project.
146
+ nextActionable(projectId, filter = {}) {
96
147
  const project = getProject(db, projectId);
97
148
  if (!project) {
98
149
  return undefined;
99
150
  }
151
+ let goalId = filter.goalId;
152
+ if (goalId === undefined) {
153
+ const active = listGoals(db, projectId).filter((goal) => goal.status === 'active');
154
+ if (active.length === 0) {
155
+ return { next_task: null, reason: 'no_active_goal', blocked: [] };
156
+ }
157
+ if (active.length > 1) {
158
+ return { next_task: null, reason: 'multiple_active_goals', blocked: [] };
159
+ }
160
+ goalId = active[0]?.id;
161
+ }
162
+ else if (!listGoals(db, projectId).some((goal) => goal.id === goalId)) {
163
+ return undefined;
164
+ }
100
165
  const tasks = listTasks(db, projectId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
101
166
  const edges = listEdges(db, projectId);
102
167
  const taskById = new Map(tasks.map((task) => [task.id, task]));
168
+ const tagsByTask = listTagsByTaskForProject(db, projectId);
169
+ const tagMatches = filter.tags !== undefined && filter.tags.length > 0
170
+ ? taskIdsWithAnyTagName(db, projectId, filter.tags.map(normalizeTagName))
171
+ : undefined;
172
+ const serialize = (task) => serializeTask(task, tagsByTask.get(task.id) ?? []);
103
173
  const prerequisites = new Map();
104
174
  for (const edge of edges) {
105
175
  const pair = prerequisiteAndDependent(edge);
@@ -116,7 +186,9 @@ export function createTaskService(deps) {
116
186
  if (tasks.length === 0) {
117
187
  return { next_task: null, reason: 'no_tasks', blocked: [] };
118
188
  }
119
- const todoTasks = tasks.filter((task) => task.status === 'todo');
189
+ const todoTasks = tasks.filter((task) => task.goalId === goalId &&
190
+ task.status === 'todo' &&
191
+ (tagMatches === undefined || tagMatches.has(task.id)));
120
192
  if (todoTasks.length === 0) {
121
193
  return { next_task: null, reason: 'no_todo_tasks', blocked: [] };
122
194
  }
@@ -141,19 +213,19 @@ export function createTaskService(deps) {
141
213
  return [...prereqs]
142
214
  .map((id) => taskById.get(id))
143
215
  .filter((task) => task !== undefined && task.status !== 'done')
144
- .map(serializeTask);
216
+ .map(serialize);
145
217
  };
146
218
  const blocked = [];
147
219
  let nextTask = null;
148
220
  for (const task of todoTasks) {
149
221
  if (isActionable(task.id)) {
150
222
  if (nextTask === null) {
151
- nextTask = serializeTask(task);
223
+ nextTask = serialize(task);
152
224
  }
153
225
  }
154
226
  else {
155
227
  blocked.push({
156
- task: serializeTask(task),
228
+ task: serialize(task),
157
229
  waiting_on: unfinishedPrereqs(task.id),
158
230
  });
159
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plandesk/api",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -18,8 +18,8 @@
18
18
  "devDependencies": {
19
19
  "typescript": "^6.0.3",
20
20
  "vitest": "^3.2.6",
21
- "@plandesk/db": "0.5.0",
22
- "@plandesk/sync-server": "0.4.2"
21
+ "@plandesk/db": "0.7.0",
22
+ "@plandesk/sync-server": "0.5.0"
23
23
  },
24
24
  "dependencies": {
25
25
  "@hono/node-server": "^2.0.4",
@@ -0,0 +1 @@
1
+ .document-editor-content img,.document-reader-content img,.portal-document-content img{max-width:100%;height:auto}.document-editor-content table,.document-reader-content table,.portal-document-content table{border-collapse:collapse;table-layout:fixed;width:100%;margin:.75rem 0}.document-editor-content th,.document-editor-content td,.document-reader-content th,.document-reader-content td,.portal-document-content th,.portal-document-content td{text-align:left;vertical-align:top;border:1px solid #d1d5db;padding:.375rem .625rem}.document-editor-content th,.document-reader-content th,.portal-document-content th{background:#f3f4f6;font-weight:600}.document-editor-content ul[data-type=taskList],.document-reader-content ul[data-type=taskList],.portal-document-content ul[data-type=taskList]{padding-left:0;list-style:none}.document-editor-content ul[data-type=taskList] li,.document-reader-content ul[data-type=taskList] li,.portal-document-content ul[data-type=taskList] li{align-items:flex-start;gap:.5rem;display:flex}.document-editor-content ul[data-type=taskList] li>label,.document-reader-content ul[data-type=taskList] li>label,.portal-document-content ul[data-type=taskList] li>label{margin-top:.15rem}.document-reader-content li[data-type=taskItem],.portal-document-content li[data-type=taskItem]{list-style:none}.document-reader-content li[data-type=taskItem]:before,.portal-document-content li[data-type=taskItem]:before{content:"☐";margin-right:.4rem}.document-reader-content li[data-type=taskItem][data-checked=true]:before,.portal-document-content li[data-type=taskItem][data-checked=true]:before{content:"☑"}.react-flow{--xy-edge-stroke-default:#b1b1b7;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#555;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#ffffff80;--xy-minimap-background-color-default:#fff;--xy-minimap-mask-background-color-default:#f0f0f099;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#e2e2e2;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:transparent;--xy-background-pattern-dots-color-default:#91919a;--xy-background-pattern-lines-color-default:#eee;--xy-background-pattern-cross-color-default:#e2e2e2;background-color:var(--xy-background-color,var(--xy-background-color-default));--xy-node-color-default:inherit;--xy-node-border-default:1px solid #1a192b;--xy-node-background-color-default:#fff;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #00000014;--xy-node-boxshadow-selected-default:0 0 0 .5px #1a192b;--xy-node-border-radius-default:3px;--xy-handle-background-color-default:#1a192b;--xy-handle-border-color-default:#fff;--xy-selection-background-color-default:#0059dc14;--xy-selection-border-default:1px dotted #0059dccc;--xy-controls-button-background-color-default:#fefefe;--xy-controls-button-background-color-hover-default:#f4f4f4;--xy-controls-button-color-default:inherit;--xy-controls-button-color-hover-default:inherit;--xy-controls-button-border-color-default:#eee;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#fff;--xy-edge-label-color-default:inherit;--xy-resize-background-color-default:#3367d9;direction:ltr}.react-flow.dark{--xy-edge-stroke-default:#3e3e3e;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#727272;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#96969640;--xy-minimap-background-color-default:#141414;--xy-minimap-mask-background-color-default:#3c3c3c99;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#2b2b2b;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:#141414;--xy-background-pattern-dots-color-default:#777;--xy-background-pattern-lines-color-default:#777;--xy-background-pattern-cross-color-default:#777;--xy-node-color-default:#f8f8f8;--xy-node-border-default:1px solid #3c3c3c;--xy-node-background-color-default:#1e1e1e;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #ffffff14;--xy-node-boxshadow-selected-default:0 0 0 .5px #999;--xy-handle-background-color-default:#bebebe;--xy-handle-border-color-default:#1e1e1e;--xy-selection-background-color-default:#c8c8dc14;--xy-selection-border-default:1px dotted #c8c8dccc;--xy-controls-button-background-color-default:#2b2b2b;--xy-controls-button-background-color-hover-default:#3e3e3e;--xy-controls-button-color-default:#f8f8f8;--xy-controls-button-color-hover-default:#fff;--xy-controls-button-border-color-default:#5b5b5b;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#141414;--xy-edge-label-color-default:#f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props,var(--xy-background-color,var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width,var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke,var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width,var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{pointer-events:none;position:absolute;overflow:visible}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected,var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:.5s linear infinite dashdraw}svg.react-flow__connectionline{z-index:1001;position:absolute;overflow:visible}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{-webkit-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default;position:absolute}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:0 0;pointer-events:none}.react-flow__nodesselection-rect{pointer-events:all;cursor:grab;position:absolute}.react-flow__handle{pointer-events:none;background-color:var(--xy-handle-background-color,var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color,var(--xy-handle-border-color-default));border-radius:100%;width:6px;min-width:5px;height:6px;min-height:5px;position:absolute}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;bottom:0;left:50%;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{z-index:5;margin:15px;position:absolute}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px)translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px)translateY(-50%)}.react-flow__attribution{background:var(--xy-attribution-background-color,var(--xy-attribution-background-color-default));margin:0;padding:2px 3px;font-size:10px}.react-flow__attribution a{color:#999;text-decoration:none}@keyframes dashdraw{0%{stroke-dashoffset:10px}}.react-flow__edgelabel-renderer{pointer-events:none;-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__viewport-portal{-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__minimap{background:var(--xy-minimap-background-color-props,var(--xy-minimap-background-color,var(--xy-minimap-background-color-default)))}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var(--xy-minimap-mask-background-color-props,var(--xy-minimap-mask-background-color,var(--xy-minimap-mask-background-color-default)));stroke:var(--xy-minimap-mask-stroke-color-props,var(--xy-minimap-mask-stroke-color,var(--xy-minimap-mask-stroke-color-default)));stroke-width:var(--xy-minimap-mask-stroke-width-props,var(--xy-minimap-mask-stroke-width,var(--xy-minimap-mask-stroke-width-default)))}.react-flow__minimap-node{fill:var(--xy-minimap-node-background-color-props,var(--xy-minimap-node-background-color,var(--xy-minimap-node-background-color-default)));stroke:var(--xy-minimap-node-stroke-color-props,var(--xy-minimap-node-stroke-color,var(--xy-minimap-node-stroke-color-default)));stroke-width:var(--xy-minimap-node-stroke-width-props,var(--xy-minimap-node-stroke-width,var(--xy-minimap-node-stroke-width-default)))}.react-flow__background-pattern.dots{fill:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-dots-color-default)))}.react-flow__background-pattern.lines{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-lines-color-default)))}.react-flow__background-pattern.cross{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-cross-color-default)))}.react-flow__controls{box-shadow:var(--xy-controls-box-shadow,var(--xy-controls-box-shadow-default));flex-direction:column;display:flex}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{background:var(--xy-controls-button-background-color,var(--xy-controls-button-background-color-default));border:none;border-bottom:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)));width:26px;height:26px;color:var(--xy-controls-button-color-props,var(--xy-controls-button-color,var(--xy-controls-button-color-default)));cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;padding:4px;display:flex}.react-flow__controls-button svg{fill:currentColor;width:100%;max-width:12px;max-height:12px}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{border-radius:var(--xy-node-border-radius,var(--xy-node-border-radius-default));width:150px;color:var(--xy-node-color,var(--xy-node-color-default));text-align:center;border:var(--xy-node-border,var(--xy-node-border-default));background-color:var(--xy-node-background-color,var(--xy-node-background-color-default));padding:10px;font-size:12px}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover,var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected,var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color,var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color,var(--xy-selection-background-color-default));border:var(--xy-selection-border,var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var(--xy-controls-button-background-color-hover-props,var(--xy-controls-button-background-color-hover,var(--xy-controls-button-background-color-hover-default)));color:var(--xy-controls-button-color-hover-props,var(--xy-controls-button-color-hover,var(--xy-controls-button-color-hover-default)))}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)))}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{background-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border:1px solid #fff;border-radius:1px;width:5px;height:5px;translate:-50% -50%}.react-flow__resize-control.handle.left{top:50%;left:0}.react-flow__resize-control.handle.right{top:50%;left:100%}.react-flow__resize-control.handle.top{top:0;left:50%}.react-flow__resize-control.handle.bottom{top:100%;left:50%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border-style:solid;border-width:0}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;height:100%;top:0;transform:translate(-50%)}.react-flow__resize-control.line.left{border-left-width:1px;left:0}.react-flow__resize-control.line.right{border-right-width:1px;left:100%}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{width:100%;height:1px;left:0;transform:translateY(-50%)}.react-flow__resize-control.line.top{border-top-width:1px;top:0}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color,var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color,var(--xy-edge-label-color-default))}