stagent 0.6.3 → 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 +226 -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 -1
- package/src/app/api/chat/entities/search/route.ts +12 -3
- package/src/app/api/projects/[id]/route.ts +37 -0
- package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
- 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/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/projects/project-form-sheet.tsx +3 -27
- package/src/components/schedules/schedule-form.tsx +5 -27
- 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 +214 -11
- 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-create-panel.tsx +5 -31
- package/src/components/tasks/task-edit-dialog.tsx +5 -27
- package/src/components/workflows/workflow-form-view.tsx +5 -29
- package/src/components/workflows/workflow-status-view.tsx +1 -1
- package/src/instrumentation.ts +3 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/tool-catalog.ts +34 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- 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 +45 -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 +227 -0
- package/src/lib/db/index.ts +9 -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 +368 -0
- 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/stagent-paths.ts +20 -0
- package/tsconfig.json +3 -1
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -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[] = [
|