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
|
@@ -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,
|
|
78
|
-
agent logs, notifications, seeded sample profiles,
|
|
79
|
-
files.
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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
|
|
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[] = [
|