stagent 0.6.2 → 0.7.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.
- package/README.md +21 -2
- package/dist/cli.js +272 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -2
- package/src/app/api/chat/entities/search/route.ts +12 -3
- 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 +72 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- 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/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- 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 +109 -2
- package/src/components/schedules/schedule-form.tsx +91 -1
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +486 -0
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-spreadsheet.tsx +499 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +111 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +103 -2
- package/src/components/workflows/workflow-form-view.tsx +207 -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 +168 -23
- package/src/instrumentation.ts +3 -0
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- 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/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +35 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +57 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +275 -0
- package/src/lib/db/index.ts +9 -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/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +445 -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/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/utils/stagent-paths.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/tsconfig.json +3 -1
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -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
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import {
|
|
4
|
+
workflowDocumentInputs,
|
|
5
|
+
documents,
|
|
6
|
+
workflows,
|
|
7
|
+
} from "@/lib/db/schema";
|
|
8
|
+
import { eq, and, inArray } from "drizzle-orm";
|
|
9
|
+
|
|
10
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/workflows/[id]/documents
|
|
14
|
+
* List all document bindings for a workflow, with document metadata.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(
|
|
17
|
+
_request: NextRequest,
|
|
18
|
+
context: RouteContext
|
|
19
|
+
) {
|
|
20
|
+
const { id: workflowId } = await context.params;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const bindings = await db
|
|
24
|
+
.select()
|
|
25
|
+
.from(workflowDocumentInputs)
|
|
26
|
+
.where(eq(workflowDocumentInputs.workflowId, workflowId));
|
|
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
|
+
const docMap = new Map(docs.map((d) => [d.id, d]));
|
|
39
|
+
|
|
40
|
+
const result = bindings.map((binding) => {
|
|
41
|
+
const doc = docMap.get(binding.documentId);
|
|
42
|
+
return {
|
|
43
|
+
bindingId: binding.id,
|
|
44
|
+
documentId: binding.documentId,
|
|
45
|
+
stepId: binding.stepId,
|
|
46
|
+
createdAt: binding.createdAt,
|
|
47
|
+
document: doc
|
|
48
|
+
? {
|
|
49
|
+
id: doc.id,
|
|
50
|
+
originalName: doc.originalName,
|
|
51
|
+
filename: doc.filename,
|
|
52
|
+
mimeType: doc.mimeType,
|
|
53
|
+
size: doc.size,
|
|
54
|
+
direction: doc.direction,
|
|
55
|
+
status: doc.status,
|
|
56
|
+
category: doc.category,
|
|
57
|
+
}
|
|
58
|
+
: null,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return NextResponse.json(result);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("[workflow-documents] GET failed:", error);
|
|
65
|
+
return NextResponse.json(
|
|
66
|
+
{ error: "Failed to fetch workflow documents" },
|
|
67
|
+
{ status: 500 }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* POST /api/workflows/[id]/documents
|
|
74
|
+
* Attach document IDs to a workflow.
|
|
75
|
+
* Body: { documentIds: string[], stepId?: string }
|
|
76
|
+
*/
|
|
77
|
+
export async function POST(
|
|
78
|
+
request: NextRequest,
|
|
79
|
+
context: RouteContext
|
|
80
|
+
) {
|
|
81
|
+
const { id: workflowId } = await context.params;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const body = await request.json();
|
|
85
|
+
const { documentIds, stepId } = body as {
|
|
86
|
+
documentIds: string[];
|
|
87
|
+
stepId?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!Array.isArray(documentIds) || documentIds.length === 0) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{ error: "documentIds must be a non-empty array" },
|
|
93
|
+
{ status: 400 }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify workflow exists
|
|
98
|
+
const [workflow] = await db
|
|
99
|
+
.select({ id: workflows.id, projectId: workflows.projectId })
|
|
100
|
+
.from(workflows)
|
|
101
|
+
.where(eq(workflows.id, workflowId));
|
|
102
|
+
|
|
103
|
+
if (!workflow) {
|
|
104
|
+
return NextResponse.json(
|
|
105
|
+
{ error: "Workflow not found" },
|
|
106
|
+
{ status: 404 }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Verify all documents exist
|
|
111
|
+
const existingDocs = await db
|
|
112
|
+
.select({ id: documents.id })
|
|
113
|
+
.from(documents)
|
|
114
|
+
.where(inArray(documents.id, documentIds));
|
|
115
|
+
|
|
116
|
+
const existingIds = new Set(existingDocs.map((d) => d.id));
|
|
117
|
+
const missing = documentIds.filter((id) => !existingIds.has(id));
|
|
118
|
+
if (missing.length > 0) {
|
|
119
|
+
return NextResponse.json(
|
|
120
|
+
{ error: `Documents not found: ${missing.join(", ")}` },
|
|
121
|
+
{ status: 404 }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Insert bindings (ignore duplicates via ON CONFLICT)
|
|
126
|
+
const now = new Date();
|
|
127
|
+
const values = documentIds.map((docId) => ({
|
|
128
|
+
id: crypto.randomUUID(),
|
|
129
|
+
workflowId,
|
|
130
|
+
documentId: docId,
|
|
131
|
+
stepId: stepId ?? null,
|
|
132
|
+
createdAt: now,
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
for (const value of values) {
|
|
136
|
+
try {
|
|
137
|
+
await db.insert(workflowDocumentInputs).values(value);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
// Skip duplicates (unique constraint violation)
|
|
140
|
+
const msg = err instanceof Error ? err.message : "";
|
|
141
|
+
if (!msg.includes("UNIQUE constraint")) throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return NextResponse.json(
|
|
146
|
+
{ attached: documentIds.length, workflowId, stepId: stepId ?? null },
|
|
147
|
+
{ status: 201 }
|
|
148
|
+
);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("[workflow-documents] POST failed:", error);
|
|
151
|
+
return NextResponse.json(
|
|
152
|
+
{ error: "Failed to attach documents" },
|
|
153
|
+
{ status: 500 }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* DELETE /api/workflows/[id]/documents
|
|
160
|
+
* Remove document bindings from a workflow.
|
|
161
|
+
* Body: { documentIds: string[], stepId?: string }
|
|
162
|
+
* If no body, removes all bindings.
|
|
163
|
+
*/
|
|
164
|
+
export async function DELETE(
|
|
165
|
+
request: NextRequest,
|
|
166
|
+
context: RouteContext
|
|
167
|
+
) {
|
|
168
|
+
const { id: workflowId } = await context.params;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
let body: { documentIds?: string[]; stepId?: string } = {};
|
|
172
|
+
try {
|
|
173
|
+
body = await request.json();
|
|
174
|
+
} catch {
|
|
175
|
+
// Empty body = remove all
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { documentIds, stepId } = body;
|
|
179
|
+
|
|
180
|
+
if (documentIds && documentIds.length > 0) {
|
|
181
|
+
// Remove specific bindings
|
|
182
|
+
for (const docId of documentIds) {
|
|
183
|
+
const conditions = [
|
|
184
|
+
eq(workflowDocumentInputs.workflowId, workflowId),
|
|
185
|
+
eq(workflowDocumentInputs.documentId, docId),
|
|
186
|
+
];
|
|
187
|
+
if (stepId !== undefined) {
|
|
188
|
+
conditions.push(eq(workflowDocumentInputs.stepId, stepId));
|
|
189
|
+
}
|
|
190
|
+
await db
|
|
191
|
+
.delete(workflowDocumentInputs)
|
|
192
|
+
.where(and(...conditions));
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// Remove all bindings for this workflow
|
|
196
|
+
await db
|
|
197
|
+
.delete(workflowDocumentInputs)
|
|
198
|
+
.where(eq(workflowDocumentInputs.workflowId, workflowId));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return NextResponse.json({ ok: true });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error("[workflow-documents] DELETE failed:", error);
|
|
204
|
+
return NextResponse.json(
|
|
205
|
+
{ error: "Failed to remove document bindings" },
|
|
206
|
+
{ status: 500 }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { workflows } from "@/lib/db/schema";
|
|
4
|
-
import { eq, and } from "drizzle-orm";
|
|
4
|
+
import { eq, and, sql } from "drizzle-orm";
|
|
5
5
|
import { executeWorkflow } from "@/lib/workflows/engine";
|
|
6
6
|
import type { WorkflowDefinition } from "@/lib/workflows/types";
|
|
7
7
|
|
|
@@ -57,7 +57,11 @@ export async function POST(
|
|
|
57
57
|
// Prevents concurrent double-execution from parallel requests.
|
|
58
58
|
const claimResult = db
|
|
59
59
|
.update(workflows)
|
|
60
|
-
.set({
|
|
60
|
+
.set({
|
|
61
|
+
status: "active",
|
|
62
|
+
runNumber: sql`${workflows.runNumber} + 1`,
|
|
63
|
+
updatedAt: new Date(),
|
|
64
|
+
})
|
|
61
65
|
.where(
|
|
62
66
|
and(
|
|
63
67
|
eq(workflows.id, id),
|
|
@@ -35,17 +35,30 @@ export async function PATCH(
|
|
|
35
35
|
return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Edit name/definition — draft
|
|
38
|
+
// Edit name/definition — draft, completed, or failed
|
|
39
39
|
if (name !== undefined || definition !== undefined) {
|
|
40
|
-
if (
|
|
40
|
+
if (!["draft", "completed", "failed"].includes(workflow.status)) {
|
|
41
41
|
return NextResponse.json(
|
|
42
|
-
{ error: "
|
|
42
|
+
{ error: "Cannot edit active or paused workflows" },
|
|
43
43
|
{ status: 409 }
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
48
48
|
|
|
49
|
+
// Reset non-draft workflows to draft and strip execution state
|
|
50
|
+
if (workflow.status !== "draft") {
|
|
51
|
+
updates.status = "draft";
|
|
52
|
+
try {
|
|
53
|
+
const existingDef = JSON.parse(workflow.definition) as Record<string, unknown>;
|
|
54
|
+
delete existingDef._state;
|
|
55
|
+
delete existingDef._loopState;
|
|
56
|
+
updates.definition = JSON.stringify(existingDef);
|
|
57
|
+
} catch {
|
|
58
|
+
// Definition will be overwritten below if provided
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
if (name !== undefined) {
|
|
50
63
|
if (!name.trim()) {
|
|
51
64
|
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
|
-
import { workflows, documents } from "@/lib/db/schema";
|
|
4
|
-
import { eq, and, inArray } from "drizzle-orm";
|
|
3
|
+
import { workflows, tasks, documents } from "@/lib/db/schema";
|
|
4
|
+
import { eq, and, inArray, count, desc, sql as drizzleSql } from "drizzle-orm";
|
|
5
5
|
import { parseWorkflowState } from "@/lib/workflows/engine";
|
|
6
6
|
|
|
7
7
|
/** Collect output documents for workflow step tasks + input documents from parent task */
|
|
@@ -81,6 +81,18 @@ export async function GET(
|
|
|
81
81
|
return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
const runHistory = await db
|
|
85
|
+
.select({
|
|
86
|
+
runNumber: tasks.workflowRunNumber,
|
|
87
|
+
taskCount: count(tasks.id),
|
|
88
|
+
completedCount: drizzleSql<number>`SUM(CASE WHEN ${tasks.status} = 'completed' THEN 1 ELSE 0 END)`,
|
|
89
|
+
failedCount: drizzleSql<number>`SUM(CASE WHEN ${tasks.status} = 'failed' THEN 1 ELSE 0 END)`,
|
|
90
|
+
})
|
|
91
|
+
.from(tasks)
|
|
92
|
+
.where(eq(tasks.workflowId, id))
|
|
93
|
+
.groupBy(tasks.workflowRunNumber)
|
|
94
|
+
.orderBy(desc(tasks.workflowRunNumber));
|
|
95
|
+
|
|
84
96
|
const { definition, state, loopState } = parseWorkflowState(workflow.definition);
|
|
85
97
|
const sourceTaskId: string | undefined = definition.sourceTaskId;
|
|
86
98
|
const { stepDocuments, parentDocuments } = await getWorkflowDocuments(state, sourceTaskId);
|
|
@@ -100,6 +112,8 @@ export async function GET(
|
|
|
100
112
|
steps: definition.steps,
|
|
101
113
|
stepDocuments,
|
|
102
114
|
parentDocuments,
|
|
115
|
+
runNumber: workflow.runNumber,
|
|
116
|
+
runHistory,
|
|
103
117
|
});
|
|
104
118
|
}
|
|
105
119
|
|
|
@@ -118,5 +132,7 @@ export async function GET(
|
|
|
118
132
|
workflowState: state,
|
|
119
133
|
stepDocuments,
|
|
120
134
|
parentDocuments,
|
|
135
|
+
runNumber: workflow.runNumber,
|
|
136
|
+
runHistory,
|
|
121
137
|
});
|
|
122
138
|
}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { workflows } from "@/lib/db/schema";
|
|
4
|
-
import { desc, eq } from "drizzle-orm";
|
|
4
|
+
import { desc, eq, sql } from "drizzle-orm";
|
|
5
5
|
import type { WorkflowDefinition } from "@/lib/workflows/types";
|
|
6
6
|
import { validateWorkflowDefinitionAssignments } from "@/lib/agents/profiles/assignment-validation";
|
|
7
7
|
import { validateWorkflowDefinition } from "@/lib/workflows/definition-validation";
|
|
8
8
|
|
|
9
9
|
export async function GET() {
|
|
10
10
|
const result = await db
|
|
11
|
-
.select(
|
|
11
|
+
.select({
|
|
12
|
+
id: workflows.id,
|
|
13
|
+
name: workflows.name,
|
|
14
|
+
projectId: workflows.projectId,
|
|
15
|
+
definition: workflows.definition,
|
|
16
|
+
status: workflows.status,
|
|
17
|
+
runNumber: workflows.runNumber,
|
|
18
|
+
createdAt: workflows.createdAt,
|
|
19
|
+
updatedAt: workflows.updatedAt,
|
|
20
|
+
taskCount: sql<number>`(SELECT COUNT(*) FROM tasks t WHERE t.workflow_id = "workflows"."id")`.as("taskCount"),
|
|
21
|
+
outputDocCount: sql<number>`(SELECT COUNT(*) FROM documents d WHERE d.task_id IN (SELECT t2.id FROM tasks t2 WHERE t2.workflow_id = "workflows"."id") AND d.direction = 'output')`.as("outputDocCount"),
|
|
22
|
+
})
|
|
12
23
|
.from(workflows)
|
|
13
24
|
.orderBy(desc(workflows.createdAt));
|
|
14
25
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { db } from "@/lib/db";
|
|
2
|
-
import { documents, tasks, projects } from "@/lib/db/schema";
|
|
2
|
+
import { documents, tasks, projects, workflows } from "@/lib/db/schema";
|
|
3
3
|
import { desc, eq } from "drizzle-orm";
|
|
4
4
|
import { DocumentBrowser } from "@/components/documents/document-browser";
|
|
5
5
|
import { PageShell } from "@/components/shared/page-shell";
|
|
@@ -31,9 +31,13 @@ export default async function DocumentsPage() {
|
|
|
31
31
|
updatedAt: documents.updatedAt,
|
|
32
32
|
taskTitle: tasks.title,
|
|
33
33
|
projectName: projects.name,
|
|
34
|
+
workflowId: workflows.id,
|
|
35
|
+
workflowName: workflows.name,
|
|
36
|
+
workflowRunNumber: tasks.workflowRunNumber,
|
|
34
37
|
})
|
|
35
38
|
.from(documents)
|
|
36
39
|
.leftJoin(tasks, eq(documents.taskId, tasks.id))
|
|
40
|
+
.leftJoin(workflows, eq(tasks.workflowId, workflows.id))
|
|
37
41
|
.leftJoin(projects, eq(documents.projectId, projects.id))
|
|
38
42
|
.orderBy(desc(documents.createdAt));
|
|
39
43
|
|
package/src/app/layout.tsx
CHANGED
|
@@ -25,7 +25,6 @@ export const metadata: Metadata = {
|
|
|
25
25
|
icons: {
|
|
26
26
|
icon: [
|
|
27
27
|
{ url: "/stagent-s-64.png", sizes: "64x64", type: "image/png" },
|
|
28
|
-
{ url: "/icon.svg", type: "image/svg+xml" },
|
|
29
28
|
],
|
|
30
29
|
apple: [
|
|
31
30
|
{ url: "/stagent-s-128.png", sizes: "128x128", type: "image/png" },
|
package/src/app/manifest.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { notFound } from "next/navigation";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
|
-
import { projects, tasks, workflows } from "@/lib/db/schema";
|
|
4
|
-
import { eq, count, getTableColumns } from "drizzle-orm";
|
|
3
|
+
import { projects, tasks, workflows, documents } from "@/lib/db/schema";
|
|
4
|
+
import { eq, count, desc, getTableColumns } from "drizzle-orm";
|
|
5
5
|
import { Badge } from "@/components/ui/badge";
|
|
6
6
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
7
|
import { COLUMN_ORDER } from "@/lib/constants/task-status";
|
|
8
8
|
import { PageShell } from "@/components/shared/page-shell";
|
|
9
9
|
import { ProjectDetailClient } from "@/components/projects/project-detail";
|
|
10
|
+
import Link from "next/link";
|
|
11
|
+
import { FileText } from "lucide-react";
|
|
10
12
|
import { Sparkline } from "@/components/charts/sparkline";
|
|
11
13
|
import { getProjectCompletionTrend } from "@/lib/queries/chart-data";
|
|
12
14
|
import { EnvironmentSummaryCard } from "@/components/environment/environment-summary-card";
|
|
@@ -38,6 +40,27 @@ export default async function ProjectDetailPage({
|
|
|
38
40
|
.where(eq(tasks.projectId, id))
|
|
39
41
|
.orderBy(tasks.priority, tasks.createdAt);
|
|
40
42
|
|
|
43
|
+
// Document count and recent docs
|
|
44
|
+
const [{ docCount }] = await db
|
|
45
|
+
.select({ docCount: count(documents.id) })
|
|
46
|
+
.from(documents)
|
|
47
|
+
.where(eq(documents.projectId, id));
|
|
48
|
+
|
|
49
|
+
const recentDocs = docCount > 0 ? await db
|
|
50
|
+
.select({
|
|
51
|
+
id: documents.id,
|
|
52
|
+
originalName: documents.originalName,
|
|
53
|
+
direction: documents.direction,
|
|
54
|
+
version: documents.version,
|
|
55
|
+
size: documents.size,
|
|
56
|
+
createdAt: documents.createdAt,
|
|
57
|
+
})
|
|
58
|
+
.from(documents)
|
|
59
|
+
.where(eq(documents.projectId, id))
|
|
60
|
+
.orderBy(desc(documents.createdAt))
|
|
61
|
+
.limit(5)
|
|
62
|
+
: [];
|
|
63
|
+
|
|
41
64
|
// Status breakdown (standalone tasks only for headline metrics)
|
|
42
65
|
const statusCounts: Record<string, number> = {};
|
|
43
66
|
const standaloneForCounts = projectTasks.filter((t) => !t.workflowId);
|
|
@@ -144,6 +167,40 @@ export default async function ProjectDetailPage({
|
|
|
144
167
|
</div>
|
|
145
168
|
)}
|
|
146
169
|
|
|
170
|
+
{/* Recent documents */}
|
|
171
|
+
{recentDocs.length > 0 && (
|
|
172
|
+
<div className="mb-6">
|
|
173
|
+
<Card>
|
|
174
|
+
<CardHeader className="pb-2">
|
|
175
|
+
<div className="flex items-center justify-between">
|
|
176
|
+
<CardTitle className="text-sm">Recent Documents</CardTitle>
|
|
177
|
+
<Link href={`/documents?projectId=${id}`} className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
|
178
|
+
View all →
|
|
179
|
+
</Link>
|
|
180
|
+
</div>
|
|
181
|
+
</CardHeader>
|
|
182
|
+
<CardContent className="divide-y divide-border">
|
|
183
|
+
{recentDocs.map((doc) => (
|
|
184
|
+
<Link
|
|
185
|
+
key={doc.id}
|
|
186
|
+
href={`/documents/${doc.id}`}
|
|
187
|
+
className="flex items-center gap-3 py-2 text-xs hover:bg-accent/50 transition-colors -mx-6 px-6"
|
|
188
|
+
>
|
|
189
|
+
<FileText className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
190
|
+
<span className="truncate flex-1">{doc.originalName}</span>
|
|
191
|
+
<Badge variant="outline" className="text-[10px]">
|
|
192
|
+
{doc.direction}
|
|
193
|
+
</Badge>
|
|
194
|
+
{doc.direction === "output" && (
|
|
195
|
+
<span className="text-muted-foreground">v{doc.version}</span>
|
|
196
|
+
)}
|
|
197
|
+
</Link>
|
|
198
|
+
))}
|
|
199
|
+
</CardContent>
|
|
200
|
+
</Card>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
147
204
|
{/* Task count summary */}
|
|
148
205
|
{(standaloneCount > 0 || workflowCount > 0) && (
|
|
149
206
|
<p className="text-xs text-muted-foreground mb-4">
|
|
@@ -151,6 +208,9 @@ export default async function ProjectDetailPage({
|
|
|
151
208
|
{workflowCount > 0 && (
|
|
152
209
|
<> · {workflowCount} workflow task{workflowCount !== 1 ? "s" : ""} across {workflowGroupCount} workflow{workflowGroupCount !== 1 ? "s" : ""}</>
|
|
153
210
|
)}
|
|
211
|
+
{docCount > 0 && (
|
|
212
|
+
<> · {docCount} document{docCount !== 1 ? "s" : ""}</>
|
|
213
|
+
)}
|
|
154
214
|
</p>
|
|
155
215
|
)}
|
|
156
216
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ProvidersAndRuntimesSection } from "@/components/settings/providers-runtimes-section";
|
|
2
2
|
import { PermissionsSections } from "@/components/settings/permissions-sections";
|
|
3
3
|
import { DataManagementSection } from "@/components/settings/data-management-section";
|
|
4
|
+
import { DatabaseSnapshotsSection } from "@/components/settings/database-snapshots-section";
|
|
4
5
|
import { BudgetGuardrailsSection } from "@/components/settings/budget-guardrails-section";
|
|
5
6
|
import { ChatSettingsSection } from "@/components/settings/chat-settings-section";
|
|
6
7
|
import { RuntimeTimeoutSection } from "@/components/settings/runtime-timeout-section";
|
|
@@ -27,6 +28,7 @@ export default function SettingsPage() {
|
|
|
27
28
|
<ChannelsSection />
|
|
28
29
|
<BudgetGuardrailsSection />
|
|
29
30
|
<PermissionsSections />
|
|
31
|
+
<DatabaseSnapshotsSection />
|
|
30
32
|
<DataManagementSection />
|
|
31
33
|
</div>
|
|
32
34
|
</PageShell>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { projects } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { getTable, listRows } from "@/lib/data/tables";
|
|
6
|
+
import { PageShell } from "@/components/shared/page-shell";
|
|
7
|
+
import { TableDetailTabs } from "@/components/tables/table-detail-tabs";
|
|
8
|
+
import { evaluateComputedColumns } from "@/lib/tables/computed";
|
|
9
|
+
import type { ColumnDef } from "@/lib/tables/types";
|
|
10
|
+
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
params: Promise<{ id: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function TableDetailPage({ params }: Props) {
|
|
18
|
+
const { id } = await params;
|
|
19
|
+
const table = await getTable(id);
|
|
20
|
+
|
|
21
|
+
if (!table) {
|
|
22
|
+
notFound();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let columns: ColumnDef[] = [];
|
|
26
|
+
try {
|
|
27
|
+
columns = JSON.parse(table.columnSchema) as ColumnDef[];
|
|
28
|
+
} catch {
|
|
29
|
+
columns = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fetch project name if table is linked to a project
|
|
33
|
+
let projectName: string | null = null;
|
|
34
|
+
if (table.projectId) {
|
|
35
|
+
const project = db
|
|
36
|
+
.select({ name: projects.name })
|
|
37
|
+
.from(projects)
|
|
38
|
+
.where(eq(projects.id, table.projectId))
|
|
39
|
+
.get();
|
|
40
|
+
projectName = project?.name ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rawRows = await listRows(id, { limit: 500 });
|
|
44
|
+
const rows = evaluateComputedColumns(columns, rawRows);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<PageShell
|
|
48
|
+
title={table.name}
|
|
49
|
+
description={table.description ?? undefined}
|
|
50
|
+
backHref="/tables"
|
|
51
|
+
backLabel="Tables"
|
|
52
|
+
>
|
|
53
|
+
<TableDetailTabs
|
|
54
|
+
tableId={id}
|
|
55
|
+
columns={columns}
|
|
56
|
+
initialRows={rows}
|
|
57
|
+
tableMeta={{
|
|
58
|
+
source: table.source,
|
|
59
|
+
projectName,
|
|
60
|
+
rowCount: table.rowCount,
|
|
61
|
+
createdAt: table.createdAt ? new Date(table.createdAt).toISOString() : null,
|
|
62
|
+
updatedAt: table.updatedAt ? new Date(table.updatedAt).toISOString() : null,
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
</PageShell>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { listTables } from "@/lib/data/tables";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { projects } from "@/lib/db/schema";
|
|
4
|
+
import { TableBrowser } from "@/components/tables/table-browser";
|
|
5
|
+
import { PageShell } from "@/components/shared/page-shell";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
export default async function TablesPage() {
|
|
10
|
+
const tables = await listTables();
|
|
11
|
+
|
|
12
|
+
const projectList = await db
|
|
13
|
+
.select({ id: projects.id, name: projects.name })
|
|
14
|
+
.from(projects);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<PageShell title="Tables">
|
|
18
|
+
<TableBrowser initialTables={tables} projects={projectList} />
|
|
19
|
+
</PageShell>
|
|
20
|
+
);
|
|
21
|
+
}
|