stagent 0.6.2 → 0.6.3
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.
- package/dist/cli.js +47 -1
- package/package.json +1 -2
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +35 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +133 -2
- package/src/components/schedules/schedule-form.tsx +113 -1
- package/src/components/shared/document-picker-sheet.tsx +283 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +137 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +125 -2
- package/src/components/workflows/workflow-form-view.tsx +231 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +167 -22
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +1 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/data/clear.ts +12 -0
- package/src/lib/db/bootstrap.ts +48 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/schema.ts +77 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
package/dist/cli.js
CHANGED
|
@@ -112,7 +112,10 @@ var STAGENT_TABLES = [
|
|
|
112
112
|
"agent_memory",
|
|
113
113
|
"channel_configs",
|
|
114
114
|
"channel_bindings",
|
|
115
|
-
"agent_messages"
|
|
115
|
+
"agent_messages",
|
|
116
|
+
"workflow_document_inputs",
|
|
117
|
+
"schedule_document_inputs",
|
|
118
|
+
"project_document_defaults"
|
|
116
119
|
];
|
|
117
120
|
function bootstrapStagentDatabase(sqlite2) {
|
|
118
121
|
sqlite2.exec(`
|
|
@@ -139,6 +142,7 @@ function bootstrapStagentDatabase(sqlite2) {
|
|
|
139
142
|
result TEXT,
|
|
140
143
|
session_id TEXT,
|
|
141
144
|
resume_count INTEGER DEFAULT 0 NOT NULL,
|
|
145
|
+
workflow_run_number INTEGER,
|
|
142
146
|
created_at INTEGER NOT NULL,
|
|
143
147
|
updated_at INTEGER NOT NULL,
|
|
144
148
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
@@ -152,6 +156,7 @@ function bootstrapStagentDatabase(sqlite2) {
|
|
|
152
156
|
name TEXT NOT NULL,
|
|
153
157
|
definition TEXT NOT NULL,
|
|
154
158
|
status TEXT DEFAULT 'draft' NOT NULL,
|
|
159
|
+
run_number INTEGER DEFAULT 0 NOT NULL,
|
|
155
160
|
created_at INTEGER NOT NULL,
|
|
156
161
|
updated_at INTEGER NOT NULL,
|
|
157
162
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
@@ -347,6 +352,8 @@ function bootstrapStagentDatabase(sqlite2) {
|
|
|
347
352
|
addColumnIfMissing(`ALTER TABLE schedules ADD COLUMN heartbeat_spent_today INTEGER DEFAULT 0 NOT NULL;`);
|
|
348
353
|
addColumnIfMissing(`ALTER TABLE schedules ADD COLUMN heartbeat_budget_reset_at INTEGER;`);
|
|
349
354
|
addColumnIfMissing(`ALTER TABLE tasks ADD COLUMN source_type TEXT;`);
|
|
355
|
+
addColumnIfMissing(`ALTER TABLE workflows ADD COLUMN run_number INTEGER DEFAULT 0 NOT NULL;`);
|
|
356
|
+
addColumnIfMissing(`ALTER TABLE tasks ADD COLUMN workflow_run_number INTEGER;`);
|
|
350
357
|
addColumnIfMissing(`ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;`);
|
|
351
358
|
addColumnIfMissing(`ALTER TABLE documents ADD COLUMN source TEXT DEFAULT 'upload';`);
|
|
352
359
|
addColumnIfMissing(`ALTER TABLE documents ADD COLUMN conversation_id TEXT REFERENCES conversations(id);`);
|
|
@@ -619,6 +626,45 @@ function bootstrapStagentDatabase(sqlite2) {
|
|
|
619
626
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_to_status ON agent_messages(to_profile_id, status);
|
|
620
627
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_task ON agent_messages(task_id);
|
|
621
628
|
`);
|
|
629
|
+
sqlite2.exec(`
|
|
630
|
+
CREATE TABLE IF NOT EXISTS workflow_document_inputs (
|
|
631
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
632
|
+
workflow_id TEXT NOT NULL,
|
|
633
|
+
document_id TEXT NOT NULL,
|
|
634
|
+
step_id TEXT,
|
|
635
|
+
created_at INTEGER NOT NULL,
|
|
636
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
637
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
CREATE INDEX IF NOT EXISTS idx_wdi_workflow ON workflow_document_inputs(workflow_id);
|
|
641
|
+
CREATE INDEX IF NOT EXISTS idx_wdi_document ON workflow_document_inputs(document_id);
|
|
642
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_wdi_workflow_doc_step ON workflow_document_inputs(workflow_id, document_id, step_id);
|
|
643
|
+
|
|
644
|
+
CREATE TABLE IF NOT EXISTS schedule_document_inputs (
|
|
645
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
646
|
+
schedule_id TEXT NOT NULL,
|
|
647
|
+
document_id TEXT NOT NULL,
|
|
648
|
+
created_at INTEGER NOT NULL,
|
|
649
|
+
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
650
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
CREATE INDEX IF NOT EXISTS idx_sdi_schedule ON schedule_document_inputs(schedule_id);
|
|
654
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_sdi_schedule_doc ON schedule_document_inputs(schedule_id, document_id);
|
|
655
|
+
|
|
656
|
+
CREATE TABLE IF NOT EXISTS project_document_defaults (
|
|
657
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
658
|
+
project_id TEXT NOT NULL,
|
|
659
|
+
document_id TEXT NOT NULL,
|
|
660
|
+
created_at INTEGER NOT NULL,
|
|
661
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
662
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
CREATE INDEX IF NOT EXISTS idx_pdd_project ON project_document_defaults(project_id);
|
|
666
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pdd_project_doc ON project_document_defaults(project_id, document_id);
|
|
667
|
+
`);
|
|
622
668
|
}
|
|
623
669
|
function hasLegacyStagentTables(sqlite2) {
|
|
624
670
|
const placeholders = STAGENT_TABLES.map(() => "?").join(", ");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stagent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"dist/",
|
|
28
28
|
"docs/",
|
|
29
29
|
"src/",
|
|
30
|
-
"public/icon.svg",
|
|
31
30
|
"public/icon-512.png",
|
|
32
31
|
"public/stagent-s-64.png",
|
|
33
32
|
"public/stagent-s-128.png",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
|
-
import { documents, tasks, projects } from "@/lib/db/schema";
|
|
3
|
+
import { documents, tasks, projects, workflows } from "@/lib/db/schema";
|
|
4
4
|
import { eq } from "drizzle-orm";
|
|
5
5
|
import { unlink } from "fs/promises";
|
|
6
6
|
import { z } from "zod/v4";
|
|
@@ -42,9 +42,13 @@ export async function GET(
|
|
|
42
42
|
updatedAt: documents.updatedAt,
|
|
43
43
|
taskTitle: tasks.title,
|
|
44
44
|
projectName: projects.name,
|
|
45
|
+
workflowId: workflows.id,
|
|
46
|
+
workflowName: workflows.name,
|
|
47
|
+
workflowRunNumber: tasks.workflowRunNumber,
|
|
45
48
|
})
|
|
46
49
|
.from(documents)
|
|
47
50
|
.leftJoin(tasks, eq(documents.taskId, tasks.id))
|
|
51
|
+
.leftJoin(workflows, eq(tasks.workflowId, workflows.id))
|
|
48
52
|
.leftJoin(projects, eq(documents.projectId, projects.id))
|
|
49
53
|
.where(eq(documents.id, id));
|
|
50
54
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { documents, tasks } from "@/lib/db/schema";
|
|
4
|
+
import { eq, and, desc } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
export async function GET(
|
|
7
|
+
_req: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
|
|
12
|
+
// First, get the document to find its originalName and projectId
|
|
13
|
+
const [doc] = await db
|
|
14
|
+
.select({
|
|
15
|
+
originalName: documents.originalName,
|
|
16
|
+
projectId: documents.projectId,
|
|
17
|
+
direction: documents.direction,
|
|
18
|
+
})
|
|
19
|
+
.from(documents)
|
|
20
|
+
.where(eq(documents.id, id));
|
|
21
|
+
|
|
22
|
+
if (!doc) {
|
|
23
|
+
return NextResponse.json({ error: "Document not found" }, { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Only output documents have version history
|
|
27
|
+
if (doc.direction !== "output" || !doc.projectId) {
|
|
28
|
+
return NextResponse.json([]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find all output documents with same originalName + projectId
|
|
32
|
+
const versions = await db
|
|
33
|
+
.select({
|
|
34
|
+
id: documents.id,
|
|
35
|
+
version: documents.version,
|
|
36
|
+
size: documents.size,
|
|
37
|
+
status: documents.status,
|
|
38
|
+
createdAt: documents.createdAt,
|
|
39
|
+
workflowRunNumber: tasks.workflowRunNumber,
|
|
40
|
+
})
|
|
41
|
+
.from(documents)
|
|
42
|
+
.leftJoin(tasks, eq(documents.taskId, tasks.id))
|
|
43
|
+
.where(
|
|
44
|
+
and(
|
|
45
|
+
eq(documents.originalName, doc.originalName),
|
|
46
|
+
eq(documents.projectId, doc.projectId),
|
|
47
|
+
eq(documents.direction, "output")
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
.orderBy(desc(documents.version));
|
|
51
|
+
|
|
52
|
+
return NextResponse.json(versions);
|
|
53
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
|
-
import { documents, tasks, projects } from "@/lib/db/schema";
|
|
3
|
+
import { documents, tasks, projects, workflows } from "@/lib/db/schema";
|
|
4
4
|
import { eq, and, like, or, desc } from "drizzle-orm";
|
|
5
5
|
import { access, stat, copyFile, mkdir } from "fs/promises";
|
|
6
6
|
import path, { basename, extname, join } from "path";
|
|
@@ -74,9 +74,13 @@ export async function GET(req: NextRequest) {
|
|
|
74
74
|
updatedAt: documents.updatedAt,
|
|
75
75
|
taskTitle: tasks.title,
|
|
76
76
|
projectName: projects.name,
|
|
77
|
+
workflowId: workflows.id,
|
|
78
|
+
workflowName: workflows.name,
|
|
79
|
+
workflowRunNumber: tasks.workflowRunNumber,
|
|
77
80
|
})
|
|
78
81
|
.from(documents)
|
|
79
82
|
.leftJoin(tasks, eq(documents.taskId, tasks.id))
|
|
83
|
+
.leftJoin(workflows, eq(tasks.workflowId, workflows.id))
|
|
80
84
|
.leftJoin(projects, eq(documents.projectId, projects.id))
|
|
81
85
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
82
86
|
.orderBy(desc(documents.createdAt));
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import {
|
|
4
|
+
projectDocumentDefaults,
|
|
5
|
+
documents,
|
|
6
|
+
projects,
|
|
7
|
+
} from "@/lib/db/schema";
|
|
8
|
+
import { eq, inArray } from "drizzle-orm";
|
|
9
|
+
|
|
10
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/projects/[id]/documents
|
|
14
|
+
* List all default document bindings for a project, with document metadata.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(
|
|
17
|
+
_request: NextRequest,
|
|
18
|
+
context: RouteContext
|
|
19
|
+
) {
|
|
20
|
+
const { id: projectId } = await context.params;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const bindings = await db
|
|
24
|
+
.select()
|
|
25
|
+
.from(projectDocumentDefaults)
|
|
26
|
+
.where(eq(projectDocumentDefaults.projectId, projectId));
|
|
27
|
+
|
|
28
|
+
if (bindings.length === 0) {
|
|
29
|
+
return NextResponse.json([]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const docIds = bindings.map((b) => b.documentId);
|
|
33
|
+
const docs = await db
|
|
34
|
+
.select()
|
|
35
|
+
.from(documents)
|
|
36
|
+
.where(inArray(documents.id, docIds));
|
|
37
|
+
|
|
38
|
+
// Return flat document list (same shape as /api/documents)
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
docs.map((doc) => ({
|
|
41
|
+
id: doc.id,
|
|
42
|
+
originalName: doc.originalName,
|
|
43
|
+
filename: doc.filename,
|
|
44
|
+
mimeType: doc.mimeType,
|
|
45
|
+
size: doc.size,
|
|
46
|
+
direction: doc.direction,
|
|
47
|
+
status: doc.status,
|
|
48
|
+
category: doc.category,
|
|
49
|
+
}))
|
|
50
|
+
);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("[project-documents] GET failed:", error);
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: "Failed to fetch project documents" },
|
|
55
|
+
{ status: 500 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* PUT /api/projects/[id]/documents
|
|
62
|
+
* Replace all default document bindings for a project.
|
|
63
|
+
* Body: { documentIds: string[] }
|
|
64
|
+
*/
|
|
65
|
+
export async function PUT(
|
|
66
|
+
request: NextRequest,
|
|
67
|
+
context: RouteContext
|
|
68
|
+
) {
|
|
69
|
+
const { id: projectId } = await context.params;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const body = await request.json();
|
|
73
|
+
const { documentIds } = body as { documentIds: string[] };
|
|
74
|
+
|
|
75
|
+
if (!Array.isArray(documentIds)) {
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: "documentIds must be an array" },
|
|
78
|
+
{ status: 400 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Verify project exists
|
|
83
|
+
const [project] = await db
|
|
84
|
+
.select({ id: projects.id })
|
|
85
|
+
.from(projects)
|
|
86
|
+
.where(eq(projects.id, projectId));
|
|
87
|
+
|
|
88
|
+
if (!project) {
|
|
89
|
+
return NextResponse.json(
|
|
90
|
+
{ error: "Project not found" },
|
|
91
|
+
{ status: 404 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Remove all existing bindings
|
|
96
|
+
await db
|
|
97
|
+
.delete(projectDocumentDefaults)
|
|
98
|
+
.where(eq(projectDocumentDefaults.projectId, projectId));
|
|
99
|
+
|
|
100
|
+
// Insert new bindings
|
|
101
|
+
const now = new Date();
|
|
102
|
+
for (const docId of documentIds) {
|
|
103
|
+
try {
|
|
104
|
+
await db.insert(projectDocumentDefaults).values({
|
|
105
|
+
id: crypto.randomUUID(),
|
|
106
|
+
projectId,
|
|
107
|
+
documentId: docId,
|
|
108
|
+
createdAt: now,
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const msg = err instanceof Error ? err.message : "";
|
|
112
|
+
if (!msg.includes("UNIQUE constraint")) throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return NextResponse.json({ updated: documentIds.length, projectId });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error("[project-documents] PUT failed:", error);
|
|
119
|
+
return NextResponse.json(
|
|
120
|
+
{ error: "Failed to update project documents" },
|
|
121
|
+
{ status: 500 }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
environmentScans,
|
|
17
17
|
chatMessages,
|
|
18
18
|
conversations,
|
|
19
|
+
projectDocumentDefaults,
|
|
19
20
|
} from "@/lib/db/schema";
|
|
20
21
|
import { eq, inArray } from "drizzle-orm";
|
|
21
22
|
import { updateProjectSchema } from "@/lib/validators/project";
|
|
@@ -42,16 +43,44 @@ export async function PATCH(
|
|
|
42
43
|
) {
|
|
43
44
|
const { id } = await params;
|
|
44
45
|
const body = await req.json();
|
|
45
|
-
|
|
46
|
+
// Extract documentIds before validation (not a project column)
|
|
47
|
+
const { documentIds, ...projectBody } = body as Record<string, unknown> & { documentIds?: string[] };
|
|
48
|
+
const parsed = updateProjectSchema.safeParse(projectBody);
|
|
46
49
|
if (!parsed.success) {
|
|
47
50
|
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
const now = new Date();
|
|
50
54
|
await db
|
|
51
55
|
.update(projects)
|
|
52
|
-
.set({ ...parsed.data, updatedAt:
|
|
56
|
+
.set({ ...parsed.data, updatedAt: now })
|
|
53
57
|
.where(eq(projects.id, id));
|
|
54
58
|
|
|
59
|
+
// Handle default document bindings
|
|
60
|
+
if (documentIds !== undefined) {
|
|
61
|
+
try {
|
|
62
|
+
// Replace all bindings
|
|
63
|
+
await db
|
|
64
|
+
.delete(projectDocumentDefaults)
|
|
65
|
+
.where(eq(projectDocumentDefaults.projectId, id));
|
|
66
|
+
for (const docId of documentIds) {
|
|
67
|
+
try {
|
|
68
|
+
await db.insert(projectDocumentDefaults).values({
|
|
69
|
+
id: crypto.randomUUID(),
|
|
70
|
+
projectId: id,
|
|
71
|
+
documentId: docId,
|
|
72
|
+
createdAt: now,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : "";
|
|
76
|
+
if (!msg.includes("UNIQUE constraint")) throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("[projects] Document defaults update failed:", err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
const [updated] = await db
|
|
56
85
|
.select()
|
|
57
86
|
.from(projects)
|
|
@@ -160,7 +189,10 @@ export async function DELETE(
|
|
|
160
189
|
.run();
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
// 6.
|
|
192
|
+
// 6. Project document defaults (junction table)
|
|
193
|
+
db.delete(projectDocumentDefaults).where(eq(projectDocumentDefaults.projectId, id)).run();
|
|
194
|
+
|
|
195
|
+
// 7. Direct project children
|
|
164
196
|
db.delete(documents).where(eq(documents.projectId, id)).run();
|
|
165
197
|
db.delete(tasks).where(eq(tasks.projectId, id)).run();
|
|
166
198
|
if (workflowIds.length > 0) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
|
-
import { schedules } from "@/lib/db/schema";
|
|
3
|
+
import { schedules, scheduleDocumentInputs } from "@/lib/db/schema";
|
|
4
4
|
import { desc, eq } from "drizzle-orm";
|
|
5
5
|
import { parseInterval, computeNextFireTime } from "@/lib/schedules/interval-parser";
|
|
6
6
|
import { parseNaturalLanguage } from "@/lib/schedules/nlp-parser";
|
|
@@ -34,6 +34,7 @@ export async function POST(req: NextRequest) {
|
|
|
34
34
|
activeHoursEnd,
|
|
35
35
|
activeTimezone,
|
|
36
36
|
heartbeatBudgetPerDay,
|
|
37
|
+
documentIds,
|
|
37
38
|
} =
|
|
38
39
|
body as {
|
|
39
40
|
name?: string;
|
|
@@ -51,6 +52,7 @@ export async function POST(req: NextRequest) {
|
|
|
51
52
|
activeHoursEnd?: number;
|
|
52
53
|
activeTimezone?: string;
|
|
53
54
|
heartbeatBudgetPerDay?: number;
|
|
55
|
+
documentIds?: string[];
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
const scheduleType = type ?? "scheduled";
|
|
@@ -167,6 +169,22 @@ export async function POST(req: NextRequest) {
|
|
|
167
169
|
updatedAt: now,
|
|
168
170
|
});
|
|
169
171
|
|
|
172
|
+
// Link documents to schedule
|
|
173
|
+
if (documentIds && documentIds.length > 0) {
|
|
174
|
+
try {
|
|
175
|
+
for (const docId of documentIds) {
|
|
176
|
+
await db.insert(scheduleDocumentInputs).values({
|
|
177
|
+
id: crypto.randomUUID(),
|
|
178
|
+
scheduleId: id,
|
|
179
|
+
documentId: docId,
|
|
180
|
+
createdAt: now,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error("[schedules] Document association failed:", err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
170
188
|
const [created] = await db
|
|
171
189
|
.select()
|
|
172
190
|
.from(schedules)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
|
-
import { tasks, projects, workflows, schedules, usageLedger } from "@/lib/db/schema";
|
|
3
|
+
import { tasks, projects, workflows, schedules, usageLedger, documents } from "@/lib/db/schema";
|
|
4
4
|
import { eq, sum, min, max } from "drizzle-orm";
|
|
5
5
|
import { updateTaskSchema } from "@/lib/validators/task";
|
|
6
6
|
import { isValidTransition, type TaskStatus } from "@/lib/constants/task-status";
|
|
@@ -106,11 +106,46 @@ export async function PATCH(
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// Extract documentIds before spreading into task update (not a task column)
|
|
110
|
+
const { documentIds, ...taskFields } = parsed.data;
|
|
111
|
+
const now = new Date();
|
|
112
|
+
|
|
109
113
|
await db
|
|
110
114
|
.update(tasks)
|
|
111
|
-
.set({ ...
|
|
115
|
+
.set({ ...taskFields, updatedAt: now })
|
|
112
116
|
.where(eq(tasks.id, id));
|
|
113
117
|
|
|
118
|
+
// Handle document linking/unlinking
|
|
119
|
+
if (documentIds !== undefined) {
|
|
120
|
+
try {
|
|
121
|
+
// Unlink documents previously linked to this task that are no longer selected
|
|
122
|
+
const currentDocs = await db
|
|
123
|
+
.select({ id: documents.id })
|
|
124
|
+
.from(documents)
|
|
125
|
+
.where(eq(documents.taskId, id));
|
|
126
|
+
const newDocSet = new Set(documentIds);
|
|
127
|
+
for (const doc of currentDocs) {
|
|
128
|
+
if (!newDocSet.has(doc.id)) {
|
|
129
|
+
await db.update(documents)
|
|
130
|
+
.set({ taskId: null, updatedAt: now })
|
|
131
|
+
.where(eq(documents.id, doc.id));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Link newly selected documents
|
|
135
|
+
for (const docId of documentIds) {
|
|
136
|
+
await db.update(documents)
|
|
137
|
+
.set({
|
|
138
|
+
taskId: id,
|
|
139
|
+
projectId: existing.projectId,
|
|
140
|
+
updatedAt: now,
|
|
141
|
+
})
|
|
142
|
+
.where(eq(documents.id, docId));
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error("[tasks] Document association failed:", err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
114
149
|
const [updated] = await db.select().from(tasks).where(eq(tasks.id, id));
|
|
115
150
|
return NextResponse.json(updated);
|
|
116
151
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { tasks } from "@/lib/db/schema";
|
|
4
|
+
import { eq, and, ne, isNull } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
export async function GET(
|
|
7
|
+
_req: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
|
|
12
|
+
const [task] = await db
|
|
13
|
+
.select({
|
|
14
|
+
workflowId: tasks.workflowId,
|
|
15
|
+
workflowRunNumber: tasks.workflowRunNumber,
|
|
16
|
+
})
|
|
17
|
+
.from(tasks)
|
|
18
|
+
.where(eq(tasks.id, id));
|
|
19
|
+
|
|
20
|
+
if (!task || !task.workflowId) {
|
|
21
|
+
return NextResponse.json([]);
|
|
22
|
+
}
|
|
23
|
+
// Match siblings by workflowId + workflowRunNumber.
|
|
24
|
+
// For pre-existing tasks (workflowRunNumber is NULL), match all tasks
|
|
25
|
+
// in the same workflow that also have NULL workflowRunNumber.
|
|
26
|
+
const runCondition = task.workflowRunNumber != null
|
|
27
|
+
? eq(tasks.workflowRunNumber, task.workflowRunNumber)
|
|
28
|
+
: isNull(tasks.workflowRunNumber);
|
|
29
|
+
|
|
30
|
+
const siblings = await db
|
|
31
|
+
.select({
|
|
32
|
+
id: tasks.id,
|
|
33
|
+
title: tasks.title,
|
|
34
|
+
status: tasks.status,
|
|
35
|
+
createdAt: tasks.createdAt,
|
|
36
|
+
})
|
|
37
|
+
.from(tasks)
|
|
38
|
+
.where(
|
|
39
|
+
and(
|
|
40
|
+
eq(tasks.workflowId, task.workflowId),
|
|
41
|
+
runCondition,
|
|
42
|
+
ne(tasks.id, id)
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
.orderBy(tasks.createdAt);
|
|
46
|
+
|
|
47
|
+
return NextResponse.json(siblings);
|
|
48
|
+
}
|
|
@@ -66,27 +66,26 @@ export async function POST(req: NextRequest) {
|
|
|
66
66
|
updatedAt: now,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
// Link
|
|
70
|
-
if (parsed.data.
|
|
69
|
+
// Link documents to this task (from document picker or legacy fileIds)
|
|
70
|
+
if (parsed.data.documentIds && parsed.data.documentIds.length > 0) {
|
|
71
71
|
try {
|
|
72
|
-
for (const
|
|
73
|
-
// Update existing document record (created by /api/uploads) to link to this task
|
|
72
|
+
for (const docId of parsed.data.documentIds) {
|
|
74
73
|
await db.update(documents)
|
|
75
74
|
.set({
|
|
76
75
|
taskId: id,
|
|
77
76
|
projectId: parsed.data.projectId ?? null,
|
|
78
77
|
updatedAt: now,
|
|
79
78
|
})
|
|
80
|
-
.where(eq(documents.id,
|
|
79
|
+
.where(eq(documents.id, docId));
|
|
81
80
|
|
|
82
81
|
// Trigger processing if not already done (fire-and-forget)
|
|
83
|
-
processDocument(
|
|
84
|
-
console.error(`[tasks] processDocument failed for ${
|
|
82
|
+
processDocument(docId).catch((err) => {
|
|
83
|
+
console.error(`[tasks] processDocument failed for ${docId}:`, err);
|
|
85
84
|
});
|
|
86
85
|
}
|
|
87
86
|
} catch (err) {
|
|
88
|
-
//
|
|
89
|
-
console.error("[tasks]
|
|
87
|
+
// Document association is best-effort — don't fail task creation
|
|
88
|
+
console.error("[tasks] Document association failed:", err);
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|