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.
Files changed (176) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +272 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -2
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/documents/[id]/route.ts +5 -1
  17. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  18. package/src/app/api/documents/route.ts +5 -1
  19. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  20. package/src/app/api/projects/[id]/route.ts +72 -3
  21. package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
  22. package/src/app/api/schedules/route.ts +19 -1
  23. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  24. package/src/app/api/snapshots/[id]/route.ts +44 -0
  25. package/src/app/api/snapshots/route.ts +54 -0
  26. package/src/app/api/snapshots/settings/route.ts +67 -0
  27. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  28. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  29. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  30. package/src/app/api/tables/[id]/export/route.ts +94 -0
  31. package/src/app/api/tables/[id]/history/route.ts +15 -0
  32. package/src/app/api/tables/[id]/import/route.ts +111 -0
  33. package/src/app/api/tables/[id]/route.ts +86 -0
  34. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  35. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  36. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  37. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  38. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  39. package/src/app/api/tables/route.ts +65 -0
  40. package/src/app/api/tables/templates/route.ts +92 -0
  41. package/src/app/api/tasks/[id]/route.ts +37 -2
  42. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  43. package/src/app/api/tasks/route.ts +8 -9
  44. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  45. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  46. package/src/app/api/workflows/[id]/route.ts +16 -3
  47. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  48. package/src/app/api/workflows/route.ts +13 -2
  49. package/src/app/documents/page.tsx +5 -1
  50. package/src/app/layout.tsx +0 -1
  51. package/src/app/manifest.ts +3 -3
  52. package/src/app/projects/[id]/page.tsx +62 -2
  53. package/src/app/settings/page.tsx +2 -0
  54. package/src/app/tables/[id]/page.tsx +67 -0
  55. package/src/app/tables/page.tsx +21 -0
  56. package/src/app/tables/templates/page.tsx +19 -0
  57. package/src/components/chat/chat-table-result.tsx +139 -0
  58. package/src/components/documents/document-browser.tsx +1 -1
  59. package/src/components/documents/document-chip-bar.tsx +17 -1
  60. package/src/components/documents/document-detail-view.tsx +51 -0
  61. package/src/components/documents/document-grid.tsx +5 -0
  62. package/src/components/documents/document-table.tsx +4 -0
  63. package/src/components/documents/types.ts +3 -0
  64. package/src/components/projects/project-form-sheet.tsx +109 -2
  65. package/src/components/schedules/schedule-form.tsx +91 -1
  66. package/src/components/settings/data-management-section.tsx +17 -12
  67. package/src/components/settings/database-snapshots-section.tsx +469 -0
  68. package/src/components/shared/app-sidebar.tsx +2 -0
  69. package/src/components/shared/document-picker-sheet.tsx +486 -0
  70. package/src/components/tables/table-browser.tsx +234 -0
  71. package/src/components/tables/table-cell-editor.tsx +226 -0
  72. package/src/components/tables/table-chart-builder.tsx +288 -0
  73. package/src/components/tables/table-chart-view.tsx +146 -0
  74. package/src/components/tables/table-column-header.tsx +103 -0
  75. package/src/components/tables/table-column-sheet.tsx +331 -0
  76. package/src/components/tables/table-create-sheet.tsx +240 -0
  77. package/src/components/tables/table-detail-sheet.tsx +144 -0
  78. package/src/components/tables/table-detail-tabs.tsx +278 -0
  79. package/src/components/tables/table-grid.tsx +61 -0
  80. package/src/components/tables/table-history-tab.tsx +148 -0
  81. package/src/components/tables/table-import-wizard.tsx +542 -0
  82. package/src/components/tables/table-list-table.tsx +95 -0
  83. package/src/components/tables/table-relation-combobox.tsx +217 -0
  84. package/src/components/tables/table-spreadsheet.tsx +499 -0
  85. package/src/components/tables/table-template-gallery.tsx +162 -0
  86. package/src/components/tables/table-template-preview.tsx +219 -0
  87. package/src/components/tables/table-toolbar.tsx +79 -0
  88. package/src/components/tables/table-triggers-tab.tsx +446 -0
  89. package/src/components/tables/types.ts +6 -0
  90. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  91. package/src/components/tables/utils.ts +29 -0
  92. package/src/components/tasks/task-card.tsx +8 -1
  93. package/src/components/tasks/task-create-panel.tsx +111 -14
  94. package/src/components/tasks/task-detail-view.tsx +47 -0
  95. package/src/components/tasks/task-edit-dialog.tsx +103 -2
  96. package/src/components/workflows/workflow-form-view.tsx +207 -7
  97. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  98. package/src/components/workflows/workflow-list.tsx +90 -45
  99. package/src/components/workflows/workflow-status-view.tsx +168 -23
  100. package/src/instrumentation.ts +3 -0
  101. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  102. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  103. package/src/lib/agents/claude-agent.ts +3 -1
  104. package/src/lib/agents/profiles/registry.ts +6 -3
  105. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  106. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  107. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  108. package/src/lib/book/chapter-generator.ts +4 -19
  109. package/src/lib/book/chapter-mapping.ts +17 -0
  110. package/src/lib/book/content.ts +5 -16
  111. package/src/lib/book/update-detector.ts +3 -16
  112. package/src/lib/chat/engine.ts +1 -0
  113. package/src/lib/chat/stagent-tools.ts +2 -0
  114. package/src/lib/chat/system-prompt.ts +9 -1
  115. package/src/lib/chat/tool-catalog.ts +35 -0
  116. package/src/lib/chat/tools/settings-tools.ts +109 -0
  117. package/src/lib/chat/tools/table-tools.ts +955 -0
  118. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  119. package/src/lib/constants/table-status.ts +68 -0
  120. package/src/lib/data/__tests__/clear.test.ts +1 -1
  121. package/src/lib/data/clear.ts +57 -0
  122. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  123. package/src/lib/data/seed-data/conversations.ts +350 -42
  124. package/src/lib/data/seed-data/documents.ts +564 -591
  125. package/src/lib/data/seed-data/learned-context.ts +101 -22
  126. package/src/lib/data/seed-data/notifications.ts +344 -70
  127. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  128. package/src/lib/data/seed-data/profiles.ts +144 -46
  129. package/src/lib/data/seed-data/projects.ts +50 -18
  130. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  131. package/src/lib/data/seed-data/schedules.ts +208 -41
  132. package/src/lib/data/seed-data/table-templates.ts +234 -0
  133. package/src/lib/data/seed-data/tasks.ts +614 -116
  134. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  135. package/src/lib/data/seed-data/user-tables.ts +203 -0
  136. package/src/lib/data/seed-data/views.ts +52 -7
  137. package/src/lib/data/seed-data/workflows.ts +231 -84
  138. package/src/lib/data/seed.ts +55 -14
  139. package/src/lib/data/tables.ts +417 -0
  140. package/src/lib/db/bootstrap.ts +275 -0
  141. package/src/lib/db/index.ts +9 -0
  142. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  143. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  144. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  145. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  146. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  147. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  148. package/src/lib/db/schema.ts +445 -0
  149. package/src/lib/docs/reader.ts +2 -3
  150. package/src/lib/documents/context-builder.ts +75 -2
  151. package/src/lib/documents/document-resolver.ts +119 -0
  152. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  153. package/src/lib/schedules/scheduler.ts +31 -1
  154. package/src/lib/snapshots/auto-backup.ts +132 -0
  155. package/src/lib/snapshots/retention.ts +64 -0
  156. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  157. package/src/lib/tables/computed.ts +61 -0
  158. package/src/lib/tables/context-builder.ts +139 -0
  159. package/src/lib/tables/formula-engine.ts +415 -0
  160. package/src/lib/tables/history.ts +115 -0
  161. package/src/lib/tables/import.ts +343 -0
  162. package/src/lib/tables/query-builder.ts +152 -0
  163. package/src/lib/tables/trigger-evaluator.ts +146 -0
  164. package/src/lib/tables/types.ts +141 -0
  165. package/src/lib/tables/validation.ts +119 -0
  166. package/src/lib/utils/app-root.ts +20 -0
  167. package/src/lib/utils/stagent-paths.ts +20 -0
  168. package/src/lib/validators/__tests__/task.test.ts +43 -10
  169. package/src/lib/validators/task.ts +7 -1
  170. package/src/lib/workflows/blueprints/registry.ts +3 -3
  171. package/src/lib/workflows/engine.ts +24 -8
  172. package/src/lib/workflows/types.ts +14 -0
  173. package/tsconfig.json +3 -1
  174. package/public/icon.svg +0 -13
  175. package/src/components/tasks/file-upload.tsx +0 -120
  176. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
- "generated": "2026-03-31T21:00:00Z",
3
- "version": 2,
2
+ "generated": "2026-04-03T22:00:00Z",
3
+ "version": 3,
4
4
  "sections": [
5
5
  {
6
6
  "slug": "home-workspace",
@@ -72,6 +72,16 @@
72
72
  "features": ["document-manager", "file-attachment-data-layer", "document-preprocessing", "agent-document-context", "document-output-generation"],
73
73
  "screengrabCount": 2
74
74
  },
75
+ {
76
+ "slug": "tables",
77
+ "title": "Tables",
78
+ "category": "feature-reference",
79
+ "path": "features/tables.md",
80
+ "route": "/tables",
81
+ "tags": ["tables", "structured-data", "spreadsheet", "charts", "triggers", "templates", "import", "export", "formulas"],
82
+ "features": ["tables-data-layer", "tables-list-page", "tables-spreadsheet-editor", "tables-document-import", "tables-template-gallery", "tables-agent-integration", "tables-chat-queries", "tables-computed-columns", "tables-cross-joins", "tables-agent-charts", "tables-workflow-triggers", "tables-nl-creation", "tables-export", "tables-versioning"],
83
+ "screengrabCount": 8
84
+ },
75
85
  {
76
86
  "slug": "monitoring",
77
87
  "title": "Monitor",
@@ -118,8 +128,8 @@
118
128
  "category": "feature-reference",
119
129
  "path": "features/settings.md",
120
130
  "route": "/settings",
121
- "tags": ["settings", "authentication", "permissions", "presets", "budgets", "browser-tools", "ollama", "channels"],
122
- "features": ["session-management", "tool-permission-persistence", "tool-permission-presets", "browser-use", "spend-budget-guardrails", "ollama-runtime-provider", "multi-channel-delivery", "bidirectional-channel-chat"],
131
+ "tags": ["settings", "authentication", "permissions", "presets", "budgets", "browser-tools", "ollama", "channels", "snapshots"],
132
+ "features": ["session-management", "tool-permission-persistence", "tool-permission-presets", "browser-use", "spend-budget-guardrails", "ollama-runtime-provider", "multi-channel-delivery", "bidirectional-channel-chat", "database-snapshot-backup"],
123
133
  "screengrabCount": 9
124
134
  },
125
135
  {
@@ -136,7 +146,7 @@
136
146
  "slug": "playbook",
137
147
  "title": "User Guide",
138
148
  "category": "feature-reference",
139
- "path": "features/playbook.md",
149
+ "path": "features/user-guide.md",
140
150
  "route": "/user-guide",
141
151
  "tags": ["user-guide", "documentation", "adoption-tracking"],
142
152
  "features": ["playbook-documentation", "documentation-adoption-tracking"],
@@ -232,8 +242,8 @@
232
242
  "persona": "personal",
233
243
  "difficulty": "beginner",
234
244
  "path": "journeys/personal-use.md",
235
- "sections": ["home-workspace", "chat", "dashboard-kanban", "projects", "schedules", "delivery-channels", "user-guide"],
236
- "stepCount": 14
245
+ "sections": ["home-workspace", "chat", "dashboard-kanban", "projects", "tables", "schedules", "delivery-channels", "user-guide"],
246
+ "stepCount": 15
237
247
  },
238
248
  {
239
249
  "slug": "work-use",
@@ -241,8 +251,8 @@
241
251
  "persona": "work",
242
252
  "difficulty": "intermediate",
243
253
  "path": "journeys/work-use.md",
244
- "sections": ["projects", "chat", "documents", "workflows", "schedules", "cost-usage", "inbox-notifications", "delivery-channels"],
245
- "stepCount": 15
254
+ "sections": ["projects", "chat", "documents", "tables", "workflows", "schedules", "cost-usage", "inbox-notifications", "delivery-channels"],
255
+ "stepCount": 16
246
256
  },
247
257
  {
248
258
  "slug": "power-user",
@@ -250,8 +260,8 @@
250
260
  "persona": "power-user",
251
261
  "difficulty": "advanced",
252
262
  "path": "journeys/power-user.md",
253
- "sections": ["dashboard-kanban", "profiles", "chat", "workflows", "schedules", "monitoring", "settings"],
254
- "stepCount": 15
263
+ "sections": ["dashboard-kanban", "profiles", "chat", "workflows", "tables", "schedules", "monitoring", "settings"],
264
+ "stepCount": 16
255
265
  },
256
266
  {
257
267
  "slug": "developer",
@@ -259,14 +269,14 @@
259
269
  "persona": "developer",
260
270
  "difficulty": "advanced",
261
271
  "path": "journeys/developer.md",
262
- "sections": ["settings", "environment", "chat", "monitoring", "profiles", "workflows", "schedules", "delivery-channels"],
263
- "stepCount": 14
272
+ "sections": ["settings", "environment", "chat", "monitoring", "profiles", "tables", "workflows", "schedules", "delivery-channels"],
273
+ "stepCount": 15
264
274
  }
265
275
  ],
266
276
  "metadata": {
267
- "totalDocs": 29,
268
- "totalScreengrabs": 44,
269
- "featuresCovered": 68,
270
- "appSections": 15
277
+ "totalDocs": 30,
278
+ "totalScreengrabs": 48,
279
+ "featuresCovered": 82,
280
+ "appSections": 16
271
281
  }
272
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stagent",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
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",
@@ -95,6 +94,7 @@
95
94
  "react-dom": "^19",
96
95
  "react-hook-form": "^7.71.2",
97
96
  "react-markdown": "^10.1.0",
97
+ "recharts": "^3.8.1",
98
98
  "remark-gfm": "^4.0.1",
99
99
  "sharp": "^0.34.5",
100
100
  "smol-toml": "^1.6.1",
@@ -102,6 +102,7 @@
102
102
  "sugar-high": "^1.0.0",
103
103
  "tailwind-merge": "^3",
104
104
  "tailwindcss": "^4",
105
+ "tar": "^7.5.13",
105
106
  "tw-animate-css": "^1",
106
107
  "typescript": "^5",
107
108
  "zod": "^4.3.6"
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
- import { projects, tasks, workflows, documents, schedules } from "@/lib/db/schema";
3
+ import { projects, tasks, workflows, documents, schedules, userTables } from "@/lib/db/schema";
4
4
  import { like, desc } from "drizzle-orm";
5
5
  import { listProfiles } from "@/lib/agents/profiles/registry";
6
6
 
@@ -25,7 +25,7 @@ export async function GET(request: Request) {
25
25
 
26
26
  const hasQuery = query.trim().length > 0;
27
27
  const pattern = hasQuery ? `%${query}%` : "";
28
- const perType = Math.max(2, Math.floor(limit / 5));
28
+ const perType = Math.max(2, Math.floor(limit / 7));
29
29
 
30
30
  const results: EntityResult[] = [];
31
31
 
@@ -45,9 +45,12 @@ export async function GET(request: Request) {
45
45
  const scheduleQuery = db
46
46
  .select({ id: schedules.id, name: schedules.name, status: schedules.status })
47
47
  .from(schedules);
48
+ const tableQuery = db
49
+ .select({ id: userTables.id, name: userTables.name, rowCount: userTables.rowCount, source: userTables.source })
50
+ .from(userTables);
48
51
 
49
52
  // Search in parallel across all entity types
50
- const [projectRows, taskRows, workflowRows, documentRows, scheduleRows] =
53
+ const [projectRows, taskRows, workflowRows, documentRows, scheduleRows, tableRows] =
51
54
  await Promise.all([
52
55
  (hasQuery ? projectQuery.where(like(projects.name, pattern)) : projectQuery)
53
56
  .orderBy(desc(projects.updatedAt))
@@ -64,6 +67,9 @@ export async function GET(request: Request) {
64
67
  (hasQuery ? scheduleQuery.where(like(schedules.name, pattern)) : scheduleQuery)
65
68
  .orderBy(desc(schedules.updatedAt))
66
69
  .limit(perType),
70
+ (hasQuery ? tableQuery.where(like(userTables.name, pattern)) : tableQuery)
71
+ .orderBy(desc(userTables.updatedAt))
72
+ .limit(perType),
67
73
  ]);
68
74
 
69
75
  for (const p of projectRows) {
@@ -81,6 +87,9 @@ export async function GET(request: Request) {
81
87
  for (const s of scheduleRows) {
82
88
  results.push({ entityType: "schedule", entityId: s.id, label: s.name, status: s.status });
83
89
  }
90
+ for (const t of tableRows) {
91
+ results.push({ entityType: "table", entityId: t.id, label: t.name, description: `${t.rowCount ?? 0} rows · ${t.source}` });
92
+ }
84
93
 
85
94
  // Search profiles in-memory (file-based registry)
86
95
  const allProfiles = listProfiles();
@@ -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,19 @@ import {
16
16
  environmentScans,
17
17
  chatMessages,
18
18
  conversations,
19
+ projectDocumentDefaults,
20
+ userTables,
21
+ userTableColumns,
22
+ userTableRows,
23
+ userTableViews,
24
+ userTableImports,
25
+ userTableRelationships,
26
+ tableDocumentInputs,
27
+ taskTableInputs,
28
+ workflowTableInputs,
29
+ scheduleTableInputs,
30
+ userTableTriggers,
31
+ userTableRowHistory,
19
32
  } from "@/lib/db/schema";
20
33
  import { eq, inArray } from "drizzle-orm";
21
34
  import { updateProjectSchema } from "@/lib/validators/project";
@@ -42,16 +55,44 @@ export async function PATCH(
42
55
  ) {
43
56
  const { id } = await params;
44
57
  const body = await req.json();
45
- const parsed = updateProjectSchema.safeParse(body);
58
+ // Extract documentIds before validation (not a project column)
59
+ const { documentIds, ...projectBody } = body as Record<string, unknown> & { documentIds?: string[] };
60
+ const parsed = updateProjectSchema.safeParse(projectBody);
46
61
  if (!parsed.success) {
47
62
  return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
48
63
  }
49
64
 
65
+ const now = new Date();
50
66
  await db
51
67
  .update(projects)
52
- .set({ ...parsed.data, updatedAt: new Date() })
68
+ .set({ ...parsed.data, updatedAt: now })
53
69
  .where(eq(projects.id, id));
54
70
 
71
+ // Handle default document bindings
72
+ if (documentIds !== undefined) {
73
+ try {
74
+ // Replace all bindings
75
+ await db
76
+ .delete(projectDocumentDefaults)
77
+ .where(eq(projectDocumentDefaults.projectId, id));
78
+ for (const docId of documentIds) {
79
+ try {
80
+ await db.insert(projectDocumentDefaults).values({
81
+ id: crypto.randomUUID(),
82
+ projectId: id,
83
+ documentId: docId,
84
+ createdAt: now,
85
+ });
86
+ } catch (err) {
87
+ const msg = err instanceof Error ? err.message : "";
88
+ if (!msg.includes("UNIQUE constraint")) throw err;
89
+ }
90
+ }
91
+ } catch (err) {
92
+ console.error("[projects] Document defaults update failed:", err);
93
+ }
94
+ }
95
+
55
96
  const [updated] = await db
56
97
  .select()
57
98
  .from(projects)
@@ -160,7 +201,35 @@ export async function DELETE(
160
201
  .run();
161
202
  }
162
203
 
163
- // 6. Direct project children
204
+ // 6. Project document defaults (junction table)
205
+ db.delete(projectDocumentDefaults).where(eq(projectDocumentDefaults.projectId, id)).run();
206
+
207
+ // 6b. User-defined tables — cascade-delete children before parent
208
+ const tableIds = db
209
+ .select({ id: userTables.id })
210
+ .from(userTables)
211
+ .where(eq(userTables.projectId, id))
212
+ .all()
213
+ .map((r) => r.id);
214
+
215
+ if (tableIds.length > 0) {
216
+ // Junction tables first
217
+ db.delete(tableDocumentInputs).where(inArray(tableDocumentInputs.tableId, tableIds)).run();
218
+ db.delete(taskTableInputs).where(inArray(taskTableInputs.tableId, tableIds)).run();
219
+ db.delete(workflowTableInputs).where(inArray(workflowTableInputs.tableId, tableIds)).run();
220
+ db.delete(scheduleTableInputs).where(inArray(scheduleTableInputs.tableId, tableIds)).run();
221
+ // Children
222
+ db.delete(userTableRowHistory).where(inArray(userTableRowHistory.tableId, tableIds)).run();
223
+ db.delete(userTableTriggers).where(inArray(userTableTriggers.tableId, tableIds)).run();
224
+ db.delete(userTableImports).where(inArray(userTableImports.tableId, tableIds)).run();
225
+ db.delete(userTableViews).where(inArray(userTableViews.tableId, tableIds)).run();
226
+ db.delete(userTableRelationships).where(inArray(userTableRelationships.fromTableId, tableIds)).run();
227
+ db.delete(userTableRows).where(inArray(userTableRows.tableId, tableIds)).run();
228
+ db.delete(userTableColumns).where(inArray(userTableColumns.tableId, tableIds)).run();
229
+ db.delete(userTables).where(inArray(userTables.id, tableIds)).run();
230
+ }
231
+
232
+ // 7. Direct project children
164
233
  db.delete(documents).where(eq(documents.projectId, id)).run();
165
234
  db.delete(tasks).where(eq(tasks.projectId, id)).run();
166
235
  if (workflowIds.length > 0) {
@@ -27,6 +27,8 @@ describe("project DELETE cascade coverage", () => {
27
27
  "environmentScans",
28
28
  "environmentCheckpoints",
29
29
  "conversations",
30
+ "projectDocumentDefaults",
31
+ "userTables",
30
32
  ];
31
33
 
32
34
  // Tables that are indirect children (FK to a table that has projectId)
@@ -38,6 +40,17 @@ describe("project DELETE cascade coverage", () => {
38
40
  { table: "chatMessages", parent: "conversations", via: "conversationId" },
39
41
  { table: "environmentArtifacts", parent: "environmentScans", via: "scanId" },
40
42
  { table: "environmentSyncOps", parent: "environmentCheckpoints", via: "checkpointId" },
43
+ { table: "userTableColumns", parent: "userTables", via: "tableId" },
44
+ { table: "userTableRows", parent: "userTables", via: "tableId" },
45
+ { table: "userTableViews", parent: "userTables", via: "tableId" },
46
+ { table: "userTableImports", parent: "userTables", via: "tableId" },
47
+ { table: "userTableRelationships", parent: "userTables", via: "fromTableId" },
48
+ { table: "tableDocumentInputs", parent: "userTables", via: "tableId" },
49
+ { table: "taskTableInputs", parent: "userTables", via: "tableId" },
50
+ { table: "workflowTableInputs", parent: "userTables", via: "tableId" },
51
+ { table: "scheduleTableInputs", parent: "userTables", via: "tableId" },
52
+ { table: "userTableTriggers", parent: "userTables", via: "tableId" },
53
+ { table: "userTableRowHistory", parent: "userTables", via: "tableId" },
41
54
  ];
42
55
 
43
56
  it("handles all tables with direct projectId FK", () => {
@@ -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)
@@ -0,0 +1,62 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ restoreFromSnapshot,
4
+ isSnapshotLocked,
5
+ } from "@/lib/snapshots/snapshot-manager";
6
+ import { db } from "@/lib/db";
7
+ import { tasks } from "@/lib/db/schema";
8
+ import { eq } from "drizzle-orm";
9
+
10
+ /** POST /api/snapshots/[id]/restore — restore from a snapshot (destructive) */
11
+ export async function POST(
12
+ _req: NextRequest,
13
+ { params }: { params: Promise<{ id: string }> }
14
+ ) {
15
+ const { id } = await params;
16
+
17
+ if (isSnapshotLocked()) {
18
+ return NextResponse.json(
19
+ { error: "Another snapshot operation is already in progress" },
20
+ { status: 409 }
21
+ );
22
+ }
23
+
24
+ // Check for running tasks
25
+ const runningTasks = await db
26
+ .select()
27
+ .from(tasks)
28
+ .where(eq(tasks.status, "running"));
29
+
30
+ if (runningTasks.length > 0) {
31
+ return NextResponse.json(
32
+ {
33
+ error: `${runningTasks.length} task(s) are currently running. Stop them before restoring.`,
34
+ runningTasks: runningTasks.map((t) => ({
35
+ id: t.id,
36
+ title: t.title,
37
+ })),
38
+ },
39
+ { status: 409 }
40
+ );
41
+ }
42
+
43
+ try {
44
+ const result = await restoreFromSnapshot(id);
45
+
46
+ return NextResponse.json({
47
+ success: true,
48
+ requiresRestart: result.requiresRestart,
49
+ preRestoreSnapshotId: result.preRestoreSnapshotId,
50
+ message:
51
+ "Restore complete. Please restart the server to load the restored database.",
52
+ });
53
+ } catch (error) {
54
+ return NextResponse.json(
55
+ {
56
+ error:
57
+ error instanceof Error ? error.message : "Failed to restore snapshot",
58
+ },
59
+ { status: 500 }
60
+ );
61
+ }
62
+ }