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
@@ -28,7 +28,7 @@ export function DataManagementSection() {
28
28
  if (data.success) {
29
29
  const d = data.deleted;
30
30
  toast.success(
31
- `Cleared ${d.projects} projects, ${d.tasks} tasks, ${d.workflows} workflows, ${d.schedules} schedules, ${d.documents} documents, ${d.conversations} conversations, ${d.chatMessages} messages, ${d.learnedContext} learned context, ${d.views} views, ${d.agentLogs} logs, ${d.notifications} notifications, ${d.sampleProfiles} sample profiles, ${d.files} files`
31
+ `Cleared ${d.projects} projects, ${d.tasks} tasks, ${d.workflows} workflows, ${d.schedules} schedules, ${d.documents} documents, ${d.conversations} conversations, ${d.chatMessages} messages, ${d.learnedContext} learned context, ${d.views} views, ${d.usageLedger} usage entries, ${d.agentLogs} logs, ${d.notifications} notifications, ${d.sampleProfiles} sample profiles, ${d.files} files`
32
32
  );
33
33
  } else {
34
34
  toast.error(`Clear failed: ${data.error}`);
@@ -48,7 +48,7 @@ export function DataManagementSection() {
48
48
  if (data.success) {
49
49
  const s = data.seeded;
50
50
  toast.success(
51
- `Seeded ${s.profiles} profiles, ${s.projects} projects, ${s.tasks} tasks, ${s.workflows} workflows, ${s.schedules} schedules, ${s.documents} documents, ${s.conversations} conversations, ${s.chatMessages} messages, ${s.learnedContext} learned context, ${s.views} views, ${s.agentLogs} logs, ${s.notifications} notifications`
51
+ `Seeded ${s.profiles} profiles, ${s.projects} projects, ${s.tasks} tasks, ${s.workflows} workflows, ${s.schedules} schedules, ${s.documents} documents, ${s.userTables} tables (${s.userTableRows} rows), ${s.conversations} conversations, ${s.chatMessages} messages, ${s.usageLedger} usage entries, ${s.learnedContext} learned context, ${s.views} views, ${s.profileTestResults} test results, ${s.repoImports} repo imports, ${s.agentLogs} logs, ${s.notifications} notifications`
52
52
  );
53
53
  } else {
54
54
  toast.error(`Seed failed: ${data.error}`);
@@ -74,9 +74,10 @@ export function DataManagementSection() {
74
74
  <div className="flex items-center gap-2">
75
75
  <p className="text-sm text-muted-foreground">
76
76
  Delete all projects, tasks, workflows, schedules, documents,
77
- conversations, chat messages, learned context, saved views,
78
- agent logs, notifications, seeded sample profiles, and uploaded
79
- files. Authentication settings are preserved.
77
+ conversations, chat messages, usage ledger, learned context,
78
+ saved views, agent logs, notifications, seeded sample profiles,
79
+ and uploaded files.{" "}
80
+ <strong>Database snapshots and authentication settings are preserved.</strong>
80
81
  </p>
81
82
  <Badge variant="destructive" className="shrink-0">Irreversible</Badge>
82
83
  </div>
@@ -98,11 +99,15 @@ export function DataManagementSection() {
98
99
 
99
100
  <div className="space-y-3">
100
101
  <p className="text-sm text-muted-foreground">
101
- Populate with 3 custom profiles, 5 realistic projects, 25 tasks
102
- across varied statuses, 5 workflows, 4 schedules, 12 documents
103
- (XLSX, PDF, DOCX, PPTX), 3 conversations with chat history,
104
- learned context, saved views, profile test results, repo imports,
105
- agent logs, and notifications. Existing data is cleared first.
102
+ Populate with 5 agent profiles, 8 projects across 3 personas
103
+ (solo founder, agency, PE ops), 48 tasks with agent profiles and
104
+ source types, 8 workflows (sequence, checkpoint, planner-executor),
105
+ 8 schedules (including 3 heartbeat monitors), 18 markdown documents
106
+ (input and output), 6 data tables with rows (pipeline, content,
107
+ health scores, KPIs, listings, campaigns), 6 conversations with
108
+ 45 messages, 45 usage ledger entries across 3 runtimes, learned
109
+ context, 6 saved views, 4 profile test results, 3 repo imports,
110
+ agent logs, and 28 notifications. Existing data is cleared first.
106
111
  </p>
107
112
  <Button
108
113
  variant="outline"
@@ -124,7 +129,7 @@ export function DataManagementSection() {
124
129
  open={clearOpen}
125
130
  onOpenChange={setClearOpen}
126
131
  title="Clear all data?"
127
- description="This will permanently delete all projects, tasks, workflows, schedules, documents, conversations, chat messages, learned context, saved views, agent logs, notifications, seeded sample profiles, and uploaded files. Authentication settings will be preserved. This action cannot be undone."
132
+ description="This will permanently delete all projects, tasks, workflows, schedules, documents, conversations, chat messages, usage ledger, learned context, saved views, agent logs, notifications, seeded sample profiles, and uploaded files. Database snapshots and authentication settings will be preserved. This action cannot be undone."
128
133
  confirmLabel="Clear All Data"
129
134
  onConfirm={handleClear}
130
135
  destructive
@@ -134,7 +139,7 @@ export function DataManagementSection() {
134
139
  open={seedOpen}
135
140
  onOpenChange={setSeedOpen}
136
141
  title="Seed sample data?"
137
- description="This will clear all existing data first, then populate with 3 custom profiles, 5 projects, 25 tasks, 5 workflows, 4 schedules, 12 documents (XLSX, PDF, DOCX, PPTX), 3 conversations with chat history, learned context, saved views, profile test results, repo imports, agent logs, and notifications. Any current data will be lost."
142
+ description="This will clear all existing data first, then populate with 5 agent profiles, 8 projects across 3 personas (solo founder, agency, PE ops), 48 tasks, 8 workflows, 8 schedules (3 heartbeat), 18 markdown documents, 6 data tables with rows, 6 conversations, 45 usage entries across 3 runtimes, learned context, saved views, profile test results, repo imports, agent logs, and 28 notifications. Any current data will be lost."
138
143
  confirmLabel="Seed Data"
139
144
  onConfirm={handleSeed}
140
145
  />
@@ -0,0 +1,469 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@/components/ui/card";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Label } from "@/components/ui/label";
14
+ import { Switch } from "@/components/ui/switch";
15
+ import { Badge } from "@/components/ui/badge";
16
+ import { Separator } from "@/components/ui/separator";
17
+ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
18
+ import { toast } from "sonner";
19
+ import {
20
+ Loader2,
21
+ Camera,
22
+ RotateCcw,
23
+ Trash2,
24
+ HardDrive,
25
+ Clock,
26
+ AlertTriangle,
27
+ } from "lucide-react";
28
+
29
+ interface Snapshot {
30
+ id: string;
31
+ label: string;
32
+ type: "manual" | "auto";
33
+ status: "in_progress" | "completed" | "failed";
34
+ filePath: string;
35
+ sizeBytes: number;
36
+ dbSizeBytes: number;
37
+ filesSizeBytes: number;
38
+ fileCount: number;
39
+ error: string | null;
40
+ createdAt: string;
41
+ filesMissing: boolean;
42
+ }
43
+
44
+ function formatBytes(bytes: number): string {
45
+ if (bytes === 0) return "0 B";
46
+ const k = 1024;
47
+ const sizes = ["B", "KB", "MB", "GB"];
48
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
49
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
50
+ }
51
+
52
+ function formatRelativeTime(dateStr: string): string {
53
+ const date = new Date(dateStr);
54
+ const now = new Date();
55
+ const diffMs = now.getTime() - date.getTime();
56
+ const diffMins = Math.floor(diffMs / 60000);
57
+ const diffHours = Math.floor(diffMins / 60);
58
+ const diffDays = Math.floor(diffHours / 24);
59
+
60
+ if (diffMins < 1) return "just now";
61
+ if (diffMins < 60) return `${diffMins}m ago`;
62
+ if (diffHours < 24) return `${diffHours}h ago`;
63
+ if (diffDays < 7) return `${diffDays}d ago`;
64
+ return date.toLocaleDateString();
65
+ }
66
+
67
+ export function DatabaseSnapshotsSection() {
68
+ const [snapshotList, setSnapshotList] = useState<Snapshot[]>([]);
69
+ const [totalBytes, setTotalBytes] = useState(0);
70
+ const [loading, setLoading] = useState(true);
71
+ const [creating, setCreating] = useState(false);
72
+ const [restoring, setRestoring] = useState(false);
73
+ const [snapshotLabel, setSnapshotLabel] = useState("");
74
+ const [deleteId, setDeleteId] = useState<string | null>(null);
75
+ const [restoreId, setRestoreId] = useState<string | null>(null);
76
+ const [restoreComplete, setRestoreComplete] = useState(false);
77
+
78
+ // Auto-backup settings
79
+ const [autoEnabled, setAutoEnabled] = useState(false);
80
+ const [autoInterval, setAutoInterval] = useState("1d");
81
+ const [maxCount, setMaxCount] = useState("10");
82
+ const [maxAgeWeeks, setMaxAgeWeeks] = useState("4");
83
+ const [savingSettings, setSavingSettings] = useState(false);
84
+
85
+ const fetchSnapshots = useCallback(async () => {
86
+ try {
87
+ const res = await fetch("/api/snapshots");
88
+ const data = await res.json();
89
+ setSnapshotList(data.snapshots || []);
90
+ setTotalBytes(data.totalBytes || 0);
91
+ } catch {
92
+ // Silent fail on fetch
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ }, []);
97
+
98
+ const fetchSettings = useCallback(async () => {
99
+ try {
100
+ const res = await fetch("/api/snapshots/settings");
101
+ if (res.ok) {
102
+ const settings = await res.json();
103
+ setAutoEnabled(settings.enabled === "true");
104
+ setAutoInterval(settings.interval || "1d");
105
+ setMaxCount(settings.maxCount || "10");
106
+ setMaxAgeWeeks(settings.maxAgeWeeks || "4");
107
+ }
108
+ } catch {
109
+ // Use defaults
110
+ }
111
+ }, []);
112
+
113
+ useEffect(() => {
114
+ fetchSnapshots();
115
+ fetchSettings();
116
+ }, [fetchSnapshots, fetchSettings]);
117
+
118
+ async function handleCreate() {
119
+ setCreating(true);
120
+ try {
121
+ const res = await fetch("/api/snapshots", {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({
125
+ label: snapshotLabel.trim() || undefined,
126
+ }),
127
+ });
128
+ const data = await res.json();
129
+ if (res.ok) {
130
+ toast.success(
131
+ `Snapshot created — ${formatBytes(data.sizeBytes)} (${data.fileCount} files)`
132
+ );
133
+ setSnapshotLabel("");
134
+ fetchSnapshots();
135
+ } else {
136
+ toast.error(data.error || "Failed to create snapshot");
137
+ }
138
+ } catch {
139
+ toast.error("Failed to create snapshot — network error");
140
+ } finally {
141
+ setCreating(false);
142
+ }
143
+ }
144
+
145
+ async function handleDelete() {
146
+ if (!deleteId) return;
147
+ try {
148
+ const res = await fetch(`/api/snapshots/${deleteId}`, {
149
+ method: "DELETE",
150
+ });
151
+ if (res.ok) {
152
+ toast.success("Snapshot deleted");
153
+ fetchSnapshots();
154
+ } else {
155
+ const data = await res.json();
156
+ toast.error(data.error || "Failed to delete snapshot");
157
+ }
158
+ } catch {
159
+ toast.error("Failed to delete — network error");
160
+ } finally {
161
+ setDeleteId(null);
162
+ }
163
+ }
164
+
165
+ async function handleRestore() {
166
+ if (!restoreId) return;
167
+ setRestoring(true);
168
+ try {
169
+ const res = await fetch(`/api/snapshots/${restoreId}/restore`, {
170
+ method: "POST",
171
+ });
172
+ const data = await res.json();
173
+ if (res.ok) {
174
+ setRestoreComplete(true);
175
+ toast.success("Restore complete — please restart the server");
176
+ } else {
177
+ toast.error(data.error || "Restore failed");
178
+ }
179
+ } catch {
180
+ toast.error("Restore failed — network error");
181
+ } finally {
182
+ setRestoring(false);
183
+ setRestoreId(null);
184
+ }
185
+ }
186
+
187
+ async function handleSaveSettings() {
188
+ setSavingSettings(true);
189
+ try {
190
+ const res = await fetch("/api/snapshots/settings", {
191
+ method: "PUT",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify({
194
+ enabled: autoEnabled ? "true" : "false",
195
+ interval: autoInterval,
196
+ maxCount,
197
+ maxAgeWeeks,
198
+ }),
199
+ });
200
+ if (res.ok) {
201
+ toast.success("Snapshot settings saved");
202
+ } else {
203
+ const data = await res.json();
204
+ toast.error(data.error || "Failed to save settings");
205
+ }
206
+ } catch {
207
+ toast.error("Failed to save settings — network error");
208
+ } finally {
209
+ setSavingSettings(false);
210
+ }
211
+ }
212
+
213
+ return (
214
+ <>
215
+ <Card className="surface-card">
216
+ <CardHeader>
217
+ <div className="flex items-center justify-between">
218
+ <div>
219
+ <CardTitle className="flex items-center gap-2">
220
+ <HardDrive className="h-5 w-5" />
221
+ Database Snapshots
222
+ </CardTitle>
223
+ <CardDescription>
224
+ Create, schedule, and restore full-state backups of your database
225
+ and files
226
+ </CardDescription>
227
+ </div>
228
+ {totalBytes > 0 && (
229
+ <Badge variant="secondary">{formatBytes(totalBytes)} used</Badge>
230
+ )}
231
+ </div>
232
+ </CardHeader>
233
+ <CardContent className="space-y-6">
234
+ {/* Restart banner */}
235
+ {restoreComplete && (
236
+ <div className="flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
237
+ <AlertTriangle className="h-5 w-5 text-amber-600 shrink-0" />
238
+ <div className="text-sm">
239
+ <p className="font-medium text-amber-800 dark:text-amber-200">
240
+ Restart required
241
+ </p>
242
+ <p className="text-amber-700 dark:text-amber-300">
243
+ The database has been restored. Please restart the server to
244
+ load the restored data.
245
+ </p>
246
+ </div>
247
+ </div>
248
+ )}
249
+
250
+ {/* Create snapshot */}
251
+ <div className="space-y-3">
252
+ <Label className="text-sm font-medium">Manual Snapshot</Label>
253
+ <div className="flex items-center gap-2">
254
+ <Input
255
+ placeholder="Optional label (e.g., Before migration)"
256
+ value={snapshotLabel}
257
+ onChange={(e) => setSnapshotLabel(e.target.value)}
258
+ className="max-w-sm"
259
+ disabled={creating}
260
+ />
261
+ <Button
262
+ onClick={handleCreate}
263
+ disabled={creating || restoreComplete}
264
+ >
265
+ {creating ? (
266
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
267
+ ) : (
268
+ <Camera className="mr-2 h-4 w-4" />
269
+ )}
270
+ {creating ? "Creating..." : "Create Snapshot"}
271
+ </Button>
272
+ </div>
273
+ </div>
274
+
275
+ <Separator />
276
+
277
+ {/* Auto-backup settings */}
278
+ <div className="space-y-4">
279
+ <Label className="text-sm font-medium">Auto-Backup</Label>
280
+ <div className="flex items-center justify-between">
281
+ <div className="space-y-0.5">
282
+ <p className="text-sm">Enable automatic backups</p>
283
+ <p className="text-xs text-muted-foreground">
284
+ Snapshots are created at the configured interval
285
+ </p>
286
+ </div>
287
+ <Switch
288
+ checked={autoEnabled}
289
+ onCheckedChange={setAutoEnabled}
290
+ />
291
+ </div>
292
+
293
+ {autoEnabled && (
294
+ <div className="grid grid-cols-3 gap-4">
295
+ <div className="space-y-2">
296
+ <Label className="text-xs text-muted-foreground">
297
+ Interval
298
+ </Label>
299
+ <Input
300
+ value={autoInterval}
301
+ onChange={(e) => setAutoInterval(e.target.value)}
302
+ placeholder="1d"
303
+ className="font-mono"
304
+ />
305
+ <p className="text-xs text-muted-foreground">
306
+ e.g., 6h, 1d, 1w
307
+ </p>
308
+ </div>
309
+ <div className="space-y-2">
310
+ <Label className="text-xs text-muted-foreground">
311
+ Keep last N snapshots
312
+ </Label>
313
+ <Input
314
+ type="number"
315
+ value={maxCount}
316
+ onChange={(e) => setMaxCount(e.target.value)}
317
+ min={1}
318
+ max={100}
319
+ />
320
+ </div>
321
+ <div className="space-y-2">
322
+ <Label className="text-xs text-muted-foreground">
323
+ Keep last N weeks
324
+ </Label>
325
+ <Input
326
+ type="number"
327
+ value={maxAgeWeeks}
328
+ onChange={(e) => setMaxAgeWeeks(e.target.value)}
329
+ min={1}
330
+ max={52}
331
+ />
332
+ </div>
333
+ </div>
334
+ )}
335
+
336
+ <Button
337
+ variant="outline"
338
+ size="sm"
339
+ onClick={handleSaveSettings}
340
+ disabled={savingSettings}
341
+ >
342
+ {savingSettings ? (
343
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
344
+ ) : (
345
+ <Clock className="mr-2 h-4 w-4" />
346
+ )}
347
+ {savingSettings ? "Saving..." : "Save Settings"}
348
+ </Button>
349
+ </div>
350
+
351
+ <Separator />
352
+
353
+ {/* Snapshot list */}
354
+ <div className="space-y-3">
355
+ <Label className="text-sm font-medium">
356
+ Snapshots ({snapshotList.length})
357
+ </Label>
358
+
359
+ {loading ? (
360
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
361
+ <Loader2 className="h-4 w-4 animate-spin" />
362
+ Loading snapshots...
363
+ </div>
364
+ ) : snapshotList.length === 0 ? (
365
+ <p className="text-sm text-muted-foreground">
366
+ No snapshots yet. Create one to get started.
367
+ </p>
368
+ ) : (
369
+ <div className="space-y-2">
370
+ {snapshotList.map((snap) => (
371
+ <div
372
+ key={snap.id}
373
+ className="flex items-center justify-between rounded-lg border p-3"
374
+ >
375
+ <div className="space-y-1 min-w-0 flex-1">
376
+ <div className="flex items-center gap-2">
377
+ <p className="text-sm font-medium truncate">
378
+ {snap.label}
379
+ </p>
380
+ <Badge
381
+ variant={
382
+ snap.type === "auto" ? "secondary" : "default"
383
+ }
384
+ className="shrink-0 text-xs"
385
+ >
386
+ {snap.type}
387
+ </Badge>
388
+ {snap.status === "failed" && (
389
+ <Badge variant="destructive" className="shrink-0 text-xs">
390
+ failed
391
+ </Badge>
392
+ )}
393
+ {snap.status === "in_progress" && (
394
+ <Badge variant="outline" className="shrink-0 text-xs">
395
+ in progress
396
+ </Badge>
397
+ )}
398
+ {snap.filesMissing && (
399
+ <Badge
400
+ variant="destructive"
401
+ className="shrink-0 text-xs"
402
+ >
403
+ files missing
404
+ </Badge>
405
+ )}
406
+ </div>
407
+ <p className="text-xs text-muted-foreground">
408
+ {formatRelativeTime(snap.createdAt)} ·{" "}
409
+ {formatBytes(snap.sizeBytes)} ·{" "}
410
+ {snap.fileCount} files
411
+ </p>
412
+ </div>
413
+
414
+ <div className="flex items-center gap-1 shrink-0">
415
+ <Button
416
+ variant="ghost"
417
+ size="sm"
418
+ onClick={() => setRestoreId(snap.id)}
419
+ disabled={
420
+ snap.status !== "completed" ||
421
+ snap.filesMissing ||
422
+ restoring ||
423
+ restoreComplete
424
+ }
425
+ title="Restore from this snapshot"
426
+ >
427
+ <RotateCcw className="h-4 w-4" />
428
+ </Button>
429
+ <Button
430
+ variant="ghost"
431
+ size="sm"
432
+ onClick={() => setDeleteId(snap.id)}
433
+ title="Delete this snapshot"
434
+ >
435
+ <Trash2 className="h-4 w-4" />
436
+ </Button>
437
+ </div>
438
+ </div>
439
+ ))}
440
+ </div>
441
+ )}
442
+ </div>
443
+ </CardContent>
444
+ </Card>
445
+
446
+ {/* Delete confirmation */}
447
+ <ConfirmDialog
448
+ open={deleteId !== null}
449
+ onOpenChange={(open) => !open && setDeleteId(null)}
450
+ title="Delete snapshot?"
451
+ description="This will permanently delete the snapshot and its files from disk. This action cannot be undone."
452
+ confirmLabel="Delete Snapshot"
453
+ onConfirm={handleDelete}
454
+ destructive
455
+ />
456
+
457
+ {/* Restore confirmation */}
458
+ <ConfirmDialog
459
+ open={restoreId !== null}
460
+ onOpenChange={(open) => !open && setRestoreId(null)}
461
+ title="Restore from snapshot?"
462
+ description="This will replace your current database and all files with the snapshot's contents. A safety snapshot of your current state will be created first. The server must be restarted after restore."
463
+ confirmLabel="Restore"
464
+ onConfirm={handleRestore}
465
+ destructive
466
+ />
467
+ </>
468
+ );
469
+ }
@@ -18,6 +18,7 @@ import {
18
18
  Globe,
19
19
  Settings,
20
20
  MessageCircle,
21
+ Table2,
21
22
  } from "lucide-react";
22
23
  import {
23
24
  Sidebar,
@@ -56,6 +57,7 @@ const workItems: NavItem[] = [
56
57
  { title: "Projects", href: "/projects", icon: FolderKanban },
57
58
  { title: "Workflows", href: "/workflows", icon: Workflow },
58
59
  { title: "Documents", href: "/documents", icon: FileText },
60
+ { title: "Tables", href: "/tables", icon: Table2, alsoMatches: ["/tables/"] },
59
61
  ];
60
62
 
61
63
  const manageItems: NavItem[] = [