feat: Web POC with ZenStack v3, Turso, and MCP Auto-Sync#29
feat: Web POC with ZenStack v3, Turso, and MCP Auto-Sync#29beshkenadze wants to merge 14 commits intomainfrom
Conversation
…g-and-drop - Add Next.js 14 + ZenStack v3 + Turso POC in apps/web-poc - Configure Tailwind CSS v4 with dark theme (slate-950) - Add shadcn/ui components (Button, Card, Dialog, Input) - Implement Kanban board with 4 columns (To Do, In Progress, Review, Done) - Add task cards with priority badges (high/medium/low) - Integrate @hello-pangea/dnd for drag-and-drop functionality - Add dialog for creating new tasks - Configure Turso embedded replica (offline-first) - Add ZenStack schema (Board, Column, Task) Refs: SPEC-013
Add syncUrl and syncInterval to DbConfig interface Add driver, syncUrl, authToken, syncInterval to Config schema Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add McpAutoSync class to wrap mutating MCP operations Integrate auto-sync into 17 mutating handlers (add, move, update, delete, etc.) Add turso-sync command for manual sync Update CLI index.ts exports Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add db-cloudflare.ts for Cloudflare Workers Turso integration Add wrangler.toml for Cloudflare deployment config Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Document sync strategy and architecture decisions Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Update package.json with ZenStack v3 and Turso dependencies Update bun.lock with resolved dependencies Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Convert wrangler.toml from JSON to proper TOML format Add missing UI dependencies (lucide-react, radix-ui) Fix next.config.js deprecated options Update build paths for open-next Add account_id to wrangler.toml for CI deployment Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
There was a problem hiding this comment.
Pull request overview
This PR introduces a web-based Kanban board proof-of-concept (POC) along with Turso database sync capabilities for the existing CLI/TUI Kaban board application. The implementation adds auto-sync functionality to the MCP server and creates a Next.js 14 web application intended to demonstrate real-time synchronization between CLI and web interfaces.
Changes:
- Added Turso database configuration options (driver, syncUrl, authToken, syncInterval) to core schemas
- Implemented MCP auto-sync middleware that triggers background Turso sync after mutating operations
- Created new
turso-synccommand for manual and automatic database synchronization - Built a Next.js 14 web POC with ZenStack v3 ORM, drag-and-drop Kanban board, and Cloudflare Workers deployment configuration
Reviewed changes
Copilot reviewed 30 out of 32 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/schemas.ts | Added Turso-specific configuration fields to ConfigSchema |
| packages/core/src/db/types.ts | Extended DbConfig interface with syncUrl and syncInterval |
| packages/cli/src/lib/mcp-auto-sync.ts | Implemented auto-sync wrapper for MCP handlers |
| packages/cli/src/commands/turso-sync.ts | Created manual Turso sync command with watch mode |
| packages/cli/src/commands/mcp.ts | Refactored MCP handlers to use auto-sync wrappers |
| packages/cli/src/index.ts | Registered turso-sync command |
| apps/web-poc/ | Complete Next.js application with Kanban UI, ZenStack schema, and Cloudflare configuration |
| docs/SYNC_ARCHITECTURE.md | Added architecture documentation for Turso sync (in Russian) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { db, taskService, boardService, linkService, markdownService, scoringService } = | ||
| await createContext(workingDirectory); |
There was a problem hiding this comment.
Database connection resource leak: The db connection created by createContext on line 542 is never closed. Each MCP request (except kaban_init) creates a new database connection that remains open, leading to potential resource exhaustion. The database connection should be closed after the request is processed, either with a try-finally block or by ensuring db.$close() is called.
| const db = await createDb(dbPath); | ||
| await initializeSchema(db); | ||
| const boardService = new BoardService(db); | ||
| await boardService.initializeBoard(config); |
There was a problem hiding this comment.
Database connection resource leak in init handler: The database connection created on line 528 is never closed. After initializing the schema and board, the connection should be closed with db.$close() to prevent resource leaks.
| try { | ||
| const { exec } = await import("node:child_process"); | ||
| exec( | ||
| `kaban turso-sync`, | ||
| { env: { ...process.env, KABAN_AUTO_SYNC: "1" } }, | ||
| (error) => { | ||
| if (error) { | ||
| console.error("[AutoSync] Failed:", error.message); | ||
| } else { | ||
| console.log("[AutoSync] Success"); | ||
| } | ||
| this.syncInProgress = false; | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| console.error("[AutoSync] Error:", error); |
There was a problem hiding this comment.
Race condition in syncInProgress flag: The syncInProgress flag is set to true synchronously but only reset asynchronously in the callback. If syncInBackground() is called multiple times in quick succession before exec() completes, the flag may not prevent concurrent syncs as intended. Additionally, if the exec() call throws before registering the callback (line 40-52), the catch block on line 55 resets the flag, but there's a window where the flag is true but no callback is registered to reset it.
| try { | |
| const { exec } = await import("node:child_process"); | |
| exec( | |
| `kaban turso-sync`, | |
| { env: { ...process.env, KABAN_AUTO_SYNC: "1" } }, | |
| (error) => { | |
| if (error) { | |
| console.error("[AutoSync] Failed:", error.message); | |
| } else { | |
| console.log("[AutoSync] Success"); | |
| } | |
| this.syncInProgress = false; | |
| } | |
| ); | |
| } catch (error) { | |
| console.error("[AutoSync] Error:", error); | |
| // Fire-and-forget; internal helper will always reset the flag. | |
| void this.runSyncProcess(); | |
| } | |
| private async runSyncProcess(): Promise<void> { | |
| try { | |
| const { exec } = await import("node:child_process"); | |
| await new Promise<void>((resolve) => { | |
| exec( | |
| `kaban turso-sync`, | |
| { env: { ...process.env, KABAN_AUTO_SYNC: "1" } }, | |
| (error) => { | |
| if (error) { | |
| console.error("[AutoSync] Failed:", error.message); | |
| } else { | |
| console.log("[AutoSync] Success"); | |
| } | |
| resolve(); | |
| } | |
| ); | |
| }); | |
| } catch (error) { | |
| console.error("[AutoSync] Error:", error); | |
| } finally { |
| const { readFileSync } = require("node:fs"); | ||
| const { join } = require("node:path"); | ||
| const configPath = join(process.cwd(), ".kaban", "config.json"); |
There was a problem hiding this comment.
Inconsistent import style mixing require and import: This function uses CommonJS require() for imports while the rest of the file uses ES6 import statements. For consistency and better TypeScript support, use ES6 imports at the top of the file instead of dynamic require() calls.
apps/web-poc/app/board/[id]/page.tsx
Outdated
| import { Plus, MoreHorizontal, Calendar, Trash2, GripVertical } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
There was a problem hiding this comment.
Unused imports: The GripVertical and CardHeader imports are not used anywhere in this component. Remove them to keep the imports clean.
| import { Plus, MoreHorizontal, Calendar, Trash2, GripVertical } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Plus, MoreHorizontal, Calendar, Trash2 } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardTitle } from "@/components/ui/card"; |
| import { dirname, join } from "path"; | ||
| import { fileURLToPath } from "url"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
|
|
There was a problem hiding this comment.
Unused variable: The __dirname variable is defined but never used in this configuration file. Consider removing it to clean up the code.
| import { dirname, join } from "path"; | |
| import { fileURLToPath } from "url"; | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); |
| import { Command } from "commander"; | ||
| import { getKabanPaths } from "../lib/context.js"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { createDb, type Config } from "@kaban-board/core"; | ||
|
|
||
| export const tursoSyncCommand = new Command("turso-sync") | ||
| .description("Sync local database with Turso Cloud") | ||
| .option("-w, --watch", "Watch for changes and sync automatically") | ||
| .option("-i, --interval <seconds>", "Sync interval in seconds (default: 30)", "30") | ||
| .action(async (options) => { | ||
| try { | ||
| const { dbPath, configPath } = getKabanPaths(); | ||
| const config: Config = JSON.parse(readFileSync(configPath, "utf-8")); | ||
|
|
||
| if (!config.driver || config.driver !== "libsql") { | ||
| console.error("Turso not configured. Run: kaban init --turso"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (!config.syncUrl) { | ||
| console.error("syncUrl not configured. Check .kaban/config.json"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const interval = parseInt(options.interval) * 1000; | ||
|
|
||
| async function performSync() { | ||
| try { | ||
| const db = await createDb({ | ||
| url: `file:${dbPath}`, | ||
| authToken: config.authToken, | ||
| syncUrl: config.syncUrl, | ||
| }); | ||
|
|
||
| const client = db.$client as { sync?: () => Promise<void> }; | ||
| if (client.sync) { | ||
| await client.sync(); | ||
| console.log(`Sync completed at ${new Date().toLocaleTimeString()}`); | ||
| } else { | ||
| console.log("Sync not available (client does not support sync)"); | ||
| } | ||
|
|
||
| await db.$close(); | ||
| } catch (error) { | ||
| console.error("Sync failed:", error instanceof Error ? error.message : error); | ||
| } | ||
| } | ||
|
|
||
| console.log("Starting Turso sync..."); | ||
| console.log(`Database: ${dbPath}`); | ||
| console.log(`Sync URL: ${config.syncUrl}`); | ||
| await performSync(); | ||
|
|
||
| if (options.watch) { | ||
| console.log(`\nWatching for changes (interval: ${options.interval}s)...`); | ||
| console.log("Press Ctrl+C to stop\n"); | ||
|
|
||
| setInterval(performSync, interval); | ||
| } | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : error); | ||
| process.exit(1); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for turso-sync command: The new turso-sync command lacks test coverage. Since the repository uses comprehensive testing, this new command should have tests to verify the sync functionality, error handling, watch mode, and interval parsing.
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { Plus, MoreHorizontal, Calendar, Trash2, GripVertical } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | ||
| import { Input } from "@/components/ui/input"; | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| DialogTrigger, | ||
| } from "@/components/ui/dialog"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { DragDropContext, Droppable, Draggable, DropResult } from "@hello-pangea/dnd"; | ||
|
|
||
| interface Task { | ||
| id: string; | ||
| title: string; | ||
| description: string; | ||
| priority: "high" | "medium" | "low"; | ||
| dueDate: string; | ||
| columnId: string; | ||
| } | ||
|
|
||
| interface Column { | ||
| id: string; | ||
| title: string; | ||
| color: string; | ||
| } | ||
|
|
||
| const COLUMNS: Column[] = [ | ||
| { id: "todo", title: "To Do", color: "bg-slate-500" }, | ||
| { id: "in-progress", title: "In Progress", color: "bg-blue-500" }, | ||
| { id: "review", title: "Review", color: "bg-amber-500" }, | ||
| { id: "done", title: "Done", color: "bg-emerald-500" }, | ||
| ]; | ||
|
|
||
| const INITIAL_TASKS: Task[] = [ | ||
| { | ||
| id: "1", | ||
| title: "Design System", | ||
| description: "Create a comprehensive design system with colors, typography, and components", | ||
| priority: "high", | ||
| dueDate: "2026-02-01", | ||
| columnId: "todo", | ||
| }, | ||
| { | ||
| id: "2", | ||
| title: "API Integration", | ||
| description: "Integrate ZenStack ORM with Turso database", | ||
| priority: "high", | ||
| dueDate: "2026-02-03", | ||
| columnId: "in-progress", | ||
| }, | ||
| { | ||
| id: "3", | ||
| title: "Authentication", | ||
| description: "Implement user authentication with Clerk or NextAuth", | ||
| priority: "medium", | ||
| dueDate: "2026-02-05", | ||
| columnId: "todo", | ||
| }, | ||
| { | ||
| id: "4", | ||
| title: "Drag & Drop", | ||
| description: "Add drag and drop functionality for task management", | ||
| priority: "medium", | ||
| dueDate: "2026-02-02", | ||
| columnId: "review", | ||
| }, | ||
| ]; | ||
|
|
||
| const PRIORITY_COLORS = { | ||
| high: "bg-red-500/20 text-red-400 border-red-500/30", | ||
| medium: "bg-amber-500/20 text-amber-400 border-amber-500/30", | ||
| low: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30", | ||
| }; | ||
|
|
||
| export default function KanbanBoard() { | ||
| const [tasks, setTasks] = useState<Task[]>(INITIAL_TASKS); | ||
| const [newTaskTitle, setNewTaskTitle] = useState(""); | ||
| const [newTaskDescription, setNewTaskDescription] = useState(""); | ||
| const [newTaskPriority, setNewTaskPriority] = useState<"high" | "medium" | "low">("medium"); | ||
| const [activeColumn, setActiveColumn] = useState<string>(""); | ||
| const [isDialogOpen, setIsDialogOpen] = useState(false); | ||
|
|
||
| const addTask = (columnId: string) => { | ||
| if (!newTaskTitle.trim()) return; | ||
|
|
||
| const newTask: Task = { | ||
| id: Date.now().toString(), | ||
| title: newTaskTitle, | ||
| description: newTaskDescription, | ||
| priority: newTaskPriority, | ||
| dueDate: new Date().toISOString().split("T")[0], | ||
| columnId, | ||
| }; | ||
|
|
||
| setTasks([...tasks, newTask]); | ||
| setNewTaskTitle(""); | ||
| setNewTaskDescription(""); | ||
| setNewTaskPriority("medium"); | ||
| setIsDialogOpen(false); | ||
| }; | ||
|
|
||
| const deleteTask = (taskId: string) => { | ||
| setTasks(tasks.filter((t) => t.id !== taskId)); | ||
| }; | ||
|
|
||
| const moveTask = (taskId: string, newColumnId: string) => { | ||
| setTasks(tasks.map((t) => (t.id === taskId ? { ...t, columnId: newColumnId } : t))); | ||
| }; | ||
|
|
||
| const onDragEnd = (result: DropResult) => { | ||
| if (!result.destination) return; | ||
|
|
||
| const { draggableId, destination } = result; | ||
| const newColumnId = destination.droppableId; | ||
|
|
||
| setTasks((prevTasks) => | ||
| prevTasks.map((t) => | ||
| t.id === draggableId ? { ...t, columnId: newColumnId } : t | ||
| ) | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <DragDropContext onDragEnd={onDragEnd}> | ||
| <div className="min-h-screen bg-slate-950 text-slate-100 p-6"> | ||
| <div className="max-w-7xl mx-auto"> | ||
| <header className="mb-8"> | ||
| <div className="flex items-center justify-between"> | ||
| <div> | ||
| <h1 className="text-3xl font-bold tracking-tight text-white mb-1"> | ||
| Kaban Board | ||
| </h1> | ||
| <p className="text-slate-400"> | ||
| ZenStack + Turso POC | ||
| </p> | ||
| </div> | ||
| <div className="flex items-center gap-4"> | ||
| <div className="flex items-center gap-2 text-sm text-slate-400"> | ||
| <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" /> | ||
| Live Sync | ||
| </div> | ||
| <Button variant="outline" size="sm"> | ||
| Share Board | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </header> | ||
|
|
||
| <div className="grid grid-cols-4 gap-6"> | ||
| {COLUMNS.map((column) => { | ||
| const columnTasks = tasks.filter((t) => t.columnId === column.id); | ||
|
|
||
| return ( | ||
| <div key={column.id} className="flex flex-col"> | ||
| <div className="flex items-center justify-between mb-4"> | ||
| <div className="flex items-center gap-2"> | ||
| <div className={cn("w-3 h-3 rounded-full", column.color)} /> | ||
| <h2 className="font-semibold text-slate-200 uppercase tracking-wider text-sm"> | ||
| {column.title} | ||
| </h2> | ||
| <span className="bg-slate-800 text-slate-400 text-xs px-2 py-0.5 rounded-full"> | ||
| {columnTasks.length} | ||
| </span> | ||
| </div> | ||
| <Button variant="ghost" size="icon" className="h-8 w-8"> | ||
| <MoreHorizontal className="h-4 w-4" /> | ||
| </Button> | ||
| </div> | ||
|
|
||
| <Droppable droppableId={column.id}> | ||
| {(provided) => ( | ||
| <div | ||
| ref={provided.innerRef} | ||
| {...provided.droppableProps} | ||
| className="space-y-3 min-h-[200px]" | ||
| > | ||
| {columnTasks.map((task, index) => ( | ||
| <Draggable key={task.id} draggableId={task.id} index={index}> | ||
| {(provided, snapshot) => ( | ||
| <div | ||
| ref={provided.innerRef} | ||
| {...provided.draggableProps} | ||
| {...provided.dragHandleProps} | ||
| style={{ | ||
| ...provided.draggableProps.style, | ||
| }} | ||
| > | ||
| <Card | ||
| className={cn( | ||
| "bg-slate-900/50 border-slate-800 hover:border-slate-700 transition-all group cursor-pointer", | ||
| snapshot.isDragging && "shadow-lg border-indigo-500/50 rotate-2" | ||
| )} | ||
| > | ||
| <CardContent className="p-4"> | ||
| <div className="flex items-start justify-between mb-2"> | ||
| <span | ||
| className={cn( | ||
| "text-xs px-2 py-0.5 rounded border", | ||
| PRIORITY_COLORS[task.priority] | ||
| )} | ||
| > | ||
| {task.priority} | ||
| </span> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity" | ||
| onClick={() => deleteTask(task.id)} | ||
| > | ||
| <Trash2 className="h-3 w-3 text-slate-500 hover:text-red-400" /> | ||
| </Button> | ||
| </div> | ||
|
|
||
| <h3 className="font-medium text-slate-200 mb-1">{task.title}</h3> | ||
| <p className="text-sm text-slate-500 line-clamp-2 mb-3"> | ||
| {task.description} | ||
| </p> | ||
|
|
||
| <div className="flex items-center justify-between text-xs text-slate-500"> | ||
| <div className="flex items-center gap-1"> | ||
| <Calendar className="h-3 w-3" /> | ||
| {task.dueDate} | ||
| </div> | ||
| <div className="flex -space-x-2"> | ||
| <div className="w-6 h-6 rounded-full bg-indigo-500/30 border border-slate-800 flex items-center justify-center text-[10px]"> | ||
| JD | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| </div> | ||
| )} | ||
| </Draggable> | ||
| ))} | ||
| {provided.placeholder} | ||
| </div> | ||
| )} | ||
| </Droppable> | ||
|
|
||
| <Dialog open={isDialogOpen && activeColumn === column.id} onOpenChange={(open: boolean) => { | ||
| setIsDialogOpen(open); | ||
| if (open) setActiveColumn(column.id); | ||
| }}> | ||
| <DialogTrigger asChild> | ||
| <Button | ||
| variant="ghost" | ||
| className="mt-4 w-full border-2 border-dashed border-slate-800 hover:border-slate-600 hover:bg-slate-900/50 h-12" | ||
| > | ||
| <Plus className="h-4 w-4 mr-2" /> | ||
| Add Task | ||
| </Button> | ||
| </DialogTrigger> | ||
| <DialogContent className="bg-slate-900 border-slate-800 text-slate-100"> | ||
| <DialogHeader> | ||
| <DialogTitle className="text-white">Add New Task</DialogTitle> | ||
| <DialogDescription className="text-slate-400"> | ||
| Create a new task in {column.title} | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
| <div className="space-y-4 py-4"> | ||
| <div className="space-y-2"> | ||
| <label className="text-sm font-medium text-slate-300">Title</label> | ||
| <Input | ||
| placeholder="Task title..." | ||
| value={newTaskTitle} | ||
| onChange={(e) => setNewTaskTitle(e.target.value)} | ||
| className="bg-slate-950 border-slate-800 text-white" | ||
| /> | ||
| </div> | ||
| <div className="space-y-2"> | ||
| <label className="text-sm font-medium text-slate-300">Description</label> | ||
| <Input | ||
| placeholder="Task description..." | ||
| value={newTaskDescription} | ||
| onChange={(e) => setNewTaskDescription(e.target.value)} | ||
| className="bg-slate-950 border-slate-800 text-white" | ||
| /> | ||
| </div> | ||
| <div className="space-y-2"> | ||
| <label className="text-sm font-medium text-slate-300">Priority</label> | ||
| <div className="flex gap-2"> | ||
| {(["low", "medium", "high"] as const).map((p) => ( | ||
| <Button | ||
| key={p} | ||
| type="button" | ||
| variant={newTaskPriority === p ? "default" : "outline"} | ||
| size="sm" | ||
| onClick={() => setNewTaskPriority(p)} | ||
| className={cn( | ||
| "capitalize", | ||
| newTaskPriority === p && PRIORITY_COLORS[p] | ||
| )} | ||
| > | ||
| {p} | ||
| </Button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <DialogFooter> | ||
| <Button | ||
| variant="outline" | ||
| onClick={() => setIsDialogOpen(false)} | ||
| className="border-slate-700" | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| onClick={() => addTask(column.id)} | ||
| disabled={!newTaskTitle.trim()} | ||
| className="bg-indigo-600 hover:bg-indigo-700" | ||
| > | ||
| Add Task | ||
| </Button> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </DragDropContext> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Web POC not integrated with database: The Kanban board page uses local useState instead of connecting to the database. The db.ts and db-cloudflare.ts files are created but never imported or used in the application. For this POC to demonstrate actual Turso sync capabilities as described in the PR, it should fetch and update tasks from the database rather than using hardcoded INITIAL_TASKS.
| driver: z.enum(["bun", "libsql"]).optional(), | ||
| syncUrl: z.string().optional(), | ||
| authToken: z.string().optional(), | ||
| syncInterval: z.number().int().positive().optional(), |
There was a problem hiding this comment.
Unused configuration field: The syncInterval field is defined in ConfigSchema and DbConfig but is never actually used in the codebase. The turso-sync command uses its own --interval option instead of reading from config.syncInterval. Either implement support for reading this from config or remove it from the schema to avoid confusion.
| @@ -0,0 +1,11 @@ | |||
| import { dirname, join } from "path"; | |||
There was a problem hiding this comment.
Unused import join.
| import { dirname, join } from "path"; | |
| import { dirname } from "path"; |
Add id field to board config with ULID validation Update DEFAULT_CONFIG with static ULID for board.id Update init command to generate ULID for new boards Update MCP init handler to generate ULID for new boards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add ulid package to CLI dependencies Required for generating board.id in init command and MCP handler Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 35 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { readFileSync } from "node:fs"; | ||
| import { createDb, type Config } from "@kaban-board/core"; | ||
|
|
||
| export const tursoSyncCommand = new Command("turso-sync") |
There was a problem hiding this comment.
A command named kaban sync already exists (defined in packages/cli/src/commands/sync.ts) for syncing TodoWrite input to the Kaban board. Adding kaban turso-sync is correct to avoid this conflict, but the documentation in docs/SYNC_ARCHITECTURE.md incorrectly refers to the Turso sync command as kaban sync instead of kaban turso-sync. This will cause confusion for users trying to follow the documentation.
apps/web-poc/app/board/[id]/page.tsx
Outdated
| const INITIAL_TASKS: Task[] = [ | ||
| { | ||
| id: "1", | ||
| title: "Design System", | ||
| description: "Create a comprehensive design system with colors, typography, and components", | ||
| priority: "high", | ||
| dueDate: "2026-02-01", | ||
| columnId: "todo", | ||
| }, | ||
| { | ||
| id: "2", | ||
| title: "API Integration", | ||
| description: "Integrate ZenStack ORM with Turso database", | ||
| priority: "high", | ||
| dueDate: "2026-02-03", | ||
| columnId: "in-progress", | ||
| }, | ||
| { | ||
| id: "3", | ||
| title: "Authentication", | ||
| description: "Implement user authentication with Clerk or NextAuth", | ||
| priority: "medium", | ||
| dueDate: "2026-02-05", | ||
| columnId: "todo", | ||
| }, | ||
| { | ||
| id: "4", | ||
| title: "Drag & Drop", | ||
| description: "Add drag and drop functionality for task management", | ||
| priority: "medium", | ||
| dueDate: "2026-02-02", | ||
| columnId: "review", | ||
| }, | ||
| ]; | ||
|
|
||
| const PRIORITY_COLORS = { | ||
| high: "bg-red-500/20 text-red-400 border-red-500/30", | ||
| medium: "bg-amber-500/20 text-amber-400 border-amber-500/30", | ||
| low: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30", | ||
| }; | ||
|
|
||
| export default function KanbanBoard() { | ||
| const [tasks, setTasks] = useState<Task[]>(INITIAL_TASKS); |
There was a problem hiding this comment.
The web POC application uses hardcoded mock data (INITIAL_TASKS defined at line 42) and has no actual database integration, despite the PR description claiming "ZenStack v3 ORM with automatic CRUD API generation" and "Turso database with embedded replicas." While lib/db.ts and lib/db-cloudflare.ts configure database clients, they are never imported or used in the application. This is a significant discrepancy between the PR description and the actual implementation - the web POC is currently just a static UI mockup.
|
|
||
| export const DEFAULT_CONFIG: Config = { | ||
| board: { | ||
| id: "01JKX8JQY3TQJJJ4TQYA9HW7S8", |
There was a problem hiding this comment.
This hardcoded board ID in the DEFAULT_CONFIG is not actually used by the BoardService.initializeBoard method, which always generates a new ULID on line 12 of packages/core/src/services/board.ts. This makes the board.id field in the config misleading, as it suggests the board ID can be predefined when it actually gets overwritten during initialization.
| const { db, taskService, boardService, linkService, markdownService, scoringService } = | ||
| await createContext(workingDirectory); |
There was a problem hiding this comment.
The database connection created at line 543 and assigned to the db variable is never closed, causing a resource leak. While the handlers are wrapped and the db is used within the request scope, the connection remains open for the entire lifetime of the MCP server. This is particularly problematic for long-running MCP servers. Consider closing the database connection after each request is handled, or implement connection pooling.
| console.log(`\nWatching for changes (interval: ${options.interval}s)...`); | ||
| console.log("Press Ctrl+C to stop\n"); | ||
|
|
||
| setInterval(performSync, interval); |
There was a problem hiding this comment.
The setInterval at line 58 is never cleared, preventing graceful shutdown. If the command is terminated with Ctrl+C, the interval will continue running until the process is forcefully killed. Consider storing the interval ID and adding process signal handlers to clear it on SIGINT/SIGTERM.
| setInterval(performSync, interval); | |
| const syncInterval = setInterval(performSync, interval); | |
| const shutdown = (signal: NodeJS.Signals) => { | |
| console.log(`\nReceived ${signal}. Stopping Turso sync...`); | |
| clearInterval(syncInterval); | |
| process.exit(0); | |
| }; | |
| process.on("SIGINT", shutdown); | |
| process.on("SIGTERM", shutdown); |
| try { | ||
| const { exec } = await import("node:child_process"); | ||
| exec( | ||
| `kaban turso-sync`, |
There was a problem hiding this comment.
The kaban turso-sync command hardcoded at line 42 may not be available in the PATH if the CLI is run from a development environment or installed in a custom location. Consider using process.execPath and resolving the command relative to the current binary location, similar to how packages/cli/src/commands/tui.ts handles finding binaries.
| this.syncInProgress = true; | ||
|
|
||
| try { | ||
| const { exec } = await import("node:child_process"); | ||
| exec( | ||
| `kaban turso-sync`, | ||
| { env: { ...process.env, KABAN_AUTO_SYNC: "1" } }, | ||
| (error) => { | ||
| if (error) { | ||
| console.error("[AutoSync] Failed:", error.message); | ||
| } else { | ||
| console.log("[AutoSync] Success"); | ||
| } | ||
| this.syncInProgress = false; | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| console.error("[AutoSync] Error:", error); | ||
| this.syncInProgress = false; | ||
| } |
There was a problem hiding this comment.
The syncInProgress flag is set to true at line 37 but only set back to false inside the callback at line 50. If the exec call fails before the callback is invoked (e.g., if exec itself throws), the flag will remain true forever, preventing all future sync attempts. Consider wrapping the exec call in a try-catch and resetting the flag in a finally block.
apps/web-poc/lib/db.ts
Outdated
|
|
||
| export const db = new ZenStackClient(schema, { | ||
| adapter: 'libsql', | ||
| url: process.env.TURSO_DATABASE_URL!, |
There was a problem hiding this comment.
The database URL is accessed using the non-null assertion operator (!) at line 10, but there's no runtime validation that the environment variable is actually set. If TURSO_DATABASE_URL is undefined, this will result in passing undefined to the ZenStackClient, which will likely cause a runtime error. Consider adding explicit validation with a clear error message before creating the client.
| export const db = new ZenStackClient(schema, { | |
| adapter: 'libsql', | |
| url: process.env.TURSO_DATABASE_URL!, | |
| const databaseUrl = process.env.TURSO_DATABASE_URL; | |
| if (!databaseUrl) { | |
| throw new Error('Environment variable TURSO_DATABASE_URL is required but was not provided.'); | |
| } | |
| export const db = new ZenStackClient(schema, { | |
| adapter: 'libsql', | |
| url: databaseUrl, |
| kaban sync # Ручная синхронизация | ||
| kaban sync --watch # Наблюдение за изменениями | ||
| kaban sync --force # Принудительный полный sync |
There was a problem hiding this comment.
The documentation references a kaban sync command at multiple locations (lines 55-57, 135, 145, 211, 233), but the actual implementation is named kaban turso-sync (as seen in packages/cli/src/commands/turso-sync.ts and packages/cli/src/index.ts line 51). This discrepancy between documentation and implementation will confuse users. The command should either be renamed to match the documentation or the documentation should be updated to reflect the actual command name.
| const client = db.$client as { sync?: () => Promise<void> }; | ||
| if (client.sync) { | ||
| await client.sync(); | ||
| console.log(`Sync completed at ${new Date().toLocaleTimeString()}`); | ||
| } else { | ||
| console.log("Sync not available (client does not support sync)"); | ||
| } |
There was a problem hiding this comment.
The sync() method is called on db.$client assuming it has this method, but this method only exists on libsql embedded replica clients, not on all libsql clients. The code checks for its existence (line 36), but when creating the database at line 29-33, there's no guarantee that an embedded replica was created. The syncUrl parameter is passed to createDb, but without checking the libsql client documentation, it's unclear if this is sufficient to enable embedded replica mode with sync support.
Add API route to fetch tasks from local Turso embedded replica Update KanbanBoard to load real data from API Add loading and error states Display board ID in header Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Run prisma db pull to generate schema from existing CLI database Includes boards, columns, tasks, task_links, audits tables Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Convert Prisma schema to ZModel format Add boards, columns, tasks, task_links, audits models Match exact CLI database schema for compatibility Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add getClient() and syncWithTurso() functions Implement CRUD operations with raw SQL Connect to CLI database via libsql embedded replica Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Integrate with libsql client via server actions Add real-time polling every 5 seconds Display actual tasks from CLI database Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 38 changed files in this pull request and generated 17 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Helper to execute queries | ||
| export async function executeQuery(query: string, params: unknown[] = []) { | ||
| const conn = createCloudflareDB(); |
There was a problem hiding this comment.
The executeQuery helper function creates a new database connection on every query execution but never closes it. This will lead to connection leaks and resource exhaustion. Either add connection cleanup (close/dispose) or reuse connections appropriately. Consider returning both the connection and result so the caller can close it, or implement proper connection pooling.
| // Helper to execute queries | |
| export async function executeQuery(query: string, params: unknown[] = []) { | |
| const conn = createCloudflareDB(); | |
| // Lazily initialized, cached database connection for reuse | |
| let cachedCloudflareDB: ReturnType<typeof connect> | null = null; | |
| export function getCloudflareDB() { | |
| if (!cachedCloudflareDB) { | |
| cachedCloudflareDB = createCloudflareDB(); | |
| } | |
| return cachedCloudflareDB; | |
| } | |
| // Helper to execute queries | |
| export async function executeQuery(query: string, params: unknown[] = []) { | |
| const conn = getCloudflareDB(); |
| const placeholders = columnIds.map(() => "?").join(","); | ||
| const tasksResult = await client.execute({ | ||
| sql: `SELECT id, title, description, column_id, position, created_by, assigned_to, created_at, updated_at, archived | ||
| FROM tasks | ||
| WHERE column_id IN (${placeholders}) AND archived = 0 | ||
| ORDER BY position ASC`, | ||
| args: columnIds, |
There was a problem hiding this comment.
SQL injection vulnerability: The placeholders variable is concatenated directly into the SQL string without proper parameterization. While columnIds comes from the database, constructing SQL with string interpolation for IN clauses is dangerous. Use proper parameterized queries. libsql/Turso should support binding arrays for IN clauses, or construct the query differently to avoid this pattern.
| export async function getBoardData(boardId: string): Promise<{ | ||
| board: { id: string; name: string } | null; | ||
| columns: ColumnData[]; | ||
| tasks: Task[]; | ||
| }> { | ||
| await syncWithTurso(); | ||
| const client = getClient(); | ||
|
|
||
| try { | ||
| const boardResult = await client.execute({ | ||
| sql: "SELECT id, name FROM boards WHERE id = ?", | ||
| args: [boardId], | ||
| }); | ||
|
|
||
| if (boardResult.rows.length === 0) { | ||
| return { board: null, columns: [], tasks: [] }; | ||
| } | ||
|
|
||
| const board = { | ||
| id: String(boardResult.rows[0].id), | ||
| name: String(boardResult.rows[0].name), | ||
| }; | ||
|
|
||
| const columnsResult = await client.execute({ | ||
| sql: "SELECT id, name, position, wip_limit FROM columns WHERE board_id = ? ORDER BY position ASC", | ||
| args: [boardId], | ||
| }); | ||
|
|
||
| const columns: ColumnData[] = columnsResult.rows.map((row) => ({ | ||
| id: String(row.id), | ||
| name: String(row.name), | ||
| position: Number(row.position), | ||
| wip_limit: row.wip_limit ? Number(row.wip_limit) : null, | ||
| tasks: [], | ||
| })); | ||
|
|
||
| const columnIds = columns.map((c) => c.id); | ||
|
|
||
| let tasks: Task[] = []; | ||
| if (columnIds.length > 0) { | ||
| const placeholders = columnIds.map(() => "?").join(","); | ||
| const tasksResult = await client.execute({ | ||
| sql: `SELECT id, title, description, column_id, position, created_by, assigned_to, created_at, updated_at, archived | ||
| FROM tasks | ||
| WHERE column_id IN (${placeholders}) AND archived = 0 | ||
| ORDER BY position ASC`, | ||
| args: columnIds, | ||
| }); | ||
|
|
||
| tasks = tasksResult.rows.map((row) => ({ | ||
| id: String(row.id), | ||
| title: String(row.title), | ||
| description: row.description ? String(row.description) : null, | ||
| column_id: String(row.column_id), | ||
| position: Number(row.position), | ||
| created_by: String(row.created_by), | ||
| assigned_to: row.assigned_to ? String(row.assigned_to) : null, | ||
| created_at: Number(row.created_at), | ||
| updated_at: Number(row.updated_at), | ||
| archived: Number(row.archived), | ||
| })); | ||
| } | ||
|
|
||
| const columnsWithTasks = columns.map((col) => ({ | ||
| ...col, | ||
| tasks: tasks.filter((t) => t.column_id === col.id), | ||
| })); | ||
|
|
||
| return { | ||
| board, | ||
| columns: columnsWithTasks, | ||
| tasks, | ||
| }; | ||
| } catch (error) { | ||
| console.error("Error fetching board:", error); | ||
| return { board: null, columns: [], tasks: [] }; | ||
| } | ||
| } | ||
|
|
||
| export async function moveTask( | ||
| taskId: string, | ||
| newColumnId: string, | ||
| newPosition: number | ||
| ): Promise<Task | null> { | ||
| const client = getClient(); | ||
| const now = Math.floor(Date.now() / 1000); | ||
|
|
||
| try { | ||
| await client.execute({ | ||
| sql: "UPDATE tasks SET column_id = ?, position = ?, updated_at = ? WHERE id = ?", | ||
| args: [newColumnId, newPosition, now, taskId], | ||
| }); | ||
|
|
||
| await syncWithTurso(); | ||
|
|
||
| const result = await client.execute({ | ||
| sql: "SELECT id, title, description, column_id, position, created_by, assigned_to, created_at, updated_at, archived FROM tasks WHERE id = ?", | ||
| args: [taskId], | ||
| }); | ||
|
|
||
| if (result.rows.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| const row = result.rows[0]; | ||
| return { | ||
| id: String(row.id), | ||
| title: String(row.title), | ||
| description: row.description ? String(row.description) : null, | ||
| column_id: String(row.column_id), | ||
| position: Number(row.position), | ||
| created_by: String(row.created_by), | ||
| assigned_to: row.assigned_to ? String(row.assigned_to) : null, | ||
| created_at: Number(row.created_at), | ||
| updated_at: Number(row.updated_at), | ||
| archived: Number(row.archived), | ||
| }; | ||
| } catch (error) { | ||
| console.error("Failed to move task:", error); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export async function addTask(data: { | ||
| title: string; | ||
| description?: string; | ||
| column_id: string; | ||
| created_by: string; | ||
| }): Promise<Task | null> { | ||
| const client = getClient(); | ||
| const now = Math.floor(Date.now() / 1000); | ||
| const id = crypto.randomUUID(); | ||
|
|
||
| try { | ||
| await client.execute({ | ||
| sql: `INSERT INTO tasks (id, title, description, column_id, position, created_by, assigned_to, created_at, updated_at, archived, version, depends_on, files, labels) | ||
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, | ||
| args: [ | ||
| id, | ||
| data.title, | ||
| data.description || null, | ||
| data.column_id, | ||
| 0, | ||
| data.created_by, | ||
| null, | ||
| now, | ||
| now, | ||
| 0, | ||
| 1, | ||
| "[]", | ||
| "[]", | ||
| "[]", | ||
| ], | ||
| }); | ||
|
|
||
| await syncWithTurso(); | ||
|
|
||
| return { | ||
| id, | ||
| title: data.title, | ||
| description: data.description || null, | ||
| column_id: data.column_id, | ||
| position: 0, | ||
| created_by: data.created_by, | ||
| assigned_to: null, | ||
| created_at: now, | ||
| updated_at: now, | ||
| archived: 0, | ||
| }; | ||
| } catch (error) { | ||
| console.error("Failed to add task:", error); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export async function archiveTask(taskId: string): Promise<boolean> { | ||
| const client = getClient(); | ||
| const now = Math.floor(Date.now() / 1000); | ||
|
|
||
| try { | ||
| await client.execute({ | ||
| sql: "UPDATE tasks SET archived = 1, archived_at = ?, updated_at = ? WHERE id = ?", | ||
| args: [now, now, taskId], | ||
| }); | ||
|
|
||
| await syncWithTurso(); | ||
|
|
||
| return true; | ||
| } catch (error) { | ||
| console.error("Failed to archive task:", error); | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
The server actions (getBoardData, moveTask, addTask, archiveTask) lack input validation. For example, boardId, taskId, newColumnId, and other user-provided inputs are passed directly to SQL queries without validation. While parameterized queries prevent SQL injection, there should still be validation for input format (e.g., ULID format), length constraints, and business logic constraints to prevent invalid data from being processed.
| const onDragEnd = async (result: DropResult) => { | ||
| if (!result.destination || !data) return; | ||
|
|
||
| const { draggableId, destination } = result; | ||
| const newColumnId = destination.droppableId; | ||
|
|
||
| setData((prev) => { | ||
| if (!prev) return null; | ||
| return { | ||
| ...prev, | ||
| tasks: prev.tasks.map((t) => | ||
| t.id === draggableId ? { ...t, column_id: newColumnId } : t | ||
| ), | ||
| columns: prev.columns.map((col) => ({ | ||
| ...col, | ||
| tasks: | ||
| col.id === newColumnId | ||
| ? [...col.tasks, prev.tasks.find((t) => t.id === draggableId)!] | ||
| : col.tasks.filter((t) => t.id !== draggableId), | ||
| })), | ||
| }; | ||
| }); | ||
|
|
||
| await moveTask(draggableId, newColumnId, destination.index); | ||
| }; |
There was a problem hiding this comment.
The onDragEnd handler updates the UI optimistically (line 77-92) but doesn't handle failures from moveTask. If the server action fails, the UI will show the task in the new column, but the database will have it in the old column, causing data inconsistency. Add error handling to revert the optimistic update if moveTask fails.
| const client = createClient({ | ||
| url: process.env.TURSO_DATABASE_URL || 'file:./local.db', | ||
| syncUrl: process.env.TURSO_SYNC_URL, | ||
| authToken: process.env.TURSO_AUTH_TOKEN, | ||
| }); |
There was a problem hiding this comment.
The API route creates a new database client on every request but never closes it (lines 12-16). This will cause connection leaks. In a serverless environment like Cloudflare Workers, each request should properly clean up its resources. Add client.close() in a finally block or use proper connection management.
| import { connect } from '@tursodatabase/serverless'; | ||
|
|
||
| // Cloudflare Workers compatible Turso client | ||
| // Uses HTTP-based connection instead of local SQLite | ||
| export function createCloudflareDB() { | ||
| const url = process.env.TURSO_DATABASE_URL; | ||
| const authToken = process.env.TURSO_AUTH_TOKEN; | ||
|
|
||
| if (!url || !authToken) { | ||
| throw new Error('TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set'); | ||
| } | ||
|
|
||
| return connect({ | ||
| url, | ||
| authToken, | ||
| }); | ||
| } | ||
|
|
||
| // Helper to execute queries | ||
| export async function executeQuery(query: string, params: unknown[] = []) { | ||
| const conn = createCloudflareDB(); | ||
| const stmt = conn.prepare(query); | ||
| return await stmt.all(params); | ||
| } |
There was a problem hiding this comment.
The db-cloudflare.ts file and @tursodatabase/serverless dependency are not used anywhere in the application. The codebase only uses @libsql/client through lib/db.ts. Either remove the unused db-cloudflare.ts file and @tursodatabase/serverless dependency, or document why they're kept for future use.
| async function performSync() { | ||
| try { | ||
| const db = await createDb({ | ||
| url: `file:${dbPath}`, | ||
| authToken: config.authToken, | ||
| syncUrl: config.syncUrl, | ||
| }); | ||
|
|
||
| const client = db.$client as { sync?: () => Promise<void> }; | ||
| if (client.sync) { | ||
| await client.sync(); | ||
| console.log(`Sync completed at ${new Date().toLocaleTimeString()}`); | ||
| } else { | ||
| console.log("Sync not available (client does not support sync)"); | ||
| } | ||
|
|
||
| await db.$close(); |
There was a problem hiding this comment.
The performSync function creates a new database connection on every sync but doesn't handle connection pooling or reuse. In watch mode, this means creating and destroying a database connection every 30 seconds, which is inefficient. Consider creating the database connection once outside performSync and reusing it, or ensure proper connection management.
|
|
||
| datasource db { | ||
| provider = "sqlite" | ||
| url = "file:/Users/akira/Projects/My/KabanProject/.kaban/board.db" |
There was a problem hiding this comment.
The datasource url contains a hardcoded absolute path to a specific user's filesystem ('/Users/akira/Projects/My/KabanProject/.kaban/board.db'). This will not work for other developers or in production environments. This should use an environment variable like 'env("DATABASE_URL")' or be changed to a relative path.
| url = "file:/Users/akira/Projects/My/KabanProject/.kaban/board.db" | |
| url = env("DATABASE_URL") |
| // Sync with remote before reading | ||
| await client.sync(); |
There was a problem hiding this comment.
The sync() call on line 19 may fail or not be supported depending on the client configuration. If syncUrl is not provided, client.sync() might throw an error or be undefined. Add proper error handling or check if sync is available before calling it, similar to how it's done in turso-sync.ts (line 35-41).
| // Sync with remote before reading | |
| await client.sync(); | |
| // Sync with remote before reading, if supported/configured | |
| if (process.env.TURSO_SYNC_URL && typeof (client as any).sync === 'function') { | |
| try { | |
| await client.sync(); | |
| } catch (syncError) { | |
| console.warn('Turso sync failed, continuing with local replica:', syncError); | |
| } | |
| } |
| @@allow('all', true) | ||
| } | ||
|
|
||
| model columns { | ||
| id String @id | ||
| board_id String | ||
| name String | ||
| position Int | ||
| wip_limit Int? | ||
| is_terminal Int @default(0) | ||
| boards boards @relation(fields: [board_id], references: [id], onDelete: NoAction, onUpdate: NoAction) | ||
| tasks tasks[] | ||
|
|
||
| @@allow('all', true) | ||
| } | ||
|
|
||
| model tasks { | ||
| id String @id | ||
| title String | ||
| description String? | ||
| column_id String | ||
| position Int | ||
| created_by String | ||
| assigned_to String? | ||
| parent_id String? | ||
| depends_on String @default("[]") | ||
| files String @default("[]") | ||
| labels String @default("[]") | ||
| blocked_reason String? | ||
| version Int @default(1) | ||
| created_at Int | ||
| updated_at Int | ||
| started_at Int? | ||
| completed_at Int? | ||
| archived Int @default(0) | ||
| archived_at Int? | ||
| board_task_id Int? | ||
| updated_by String? | ||
| due_date Int? | ||
| task_links_task_links_to_task_idTotasks task_links[] @relation("task_links_to_task_idTotasks") | ||
| task_links_task_links_from_task_idTotasks task_links[] @relation("task_links_from_task_idTotasks") | ||
| parent tasks? @relation("tasksTotasks", fields: [parent_id], references: [id], onDelete: NoAction, onUpdate: NoAction) | ||
| subtasks tasks[] @relation("tasksTotasks") | ||
| columns columns @relation(fields: [column_id], references: [id], onDelete: NoAction, onUpdate: NoAction) | ||
|
|
||
| @@allow('all', true) | ||
| @@index([archived], map: "idx_tasks_archived") | ||
| @@index([parent_id], map: "idx_tasks_parent") | ||
| @@index([column_id], map: "idx_tasks_column") | ||
| } | ||
|
|
||
| model task_links { | ||
| id Int @id @default(autoincrement()) | ||
| from_task_id String | ||
| to_task_id String | ||
| link_type String | ||
| created_at Int @default(dbgenerated("unixepoch()")) | ||
| from_task tasks @relation("task_links_from_task_idTotasks", fields: [from_task_id], references: [id], onDelete: Cascade, onUpdate: NoAction) | ||
| to_task tasks @relation("task_links_to_task_idTotasks", fields: [to_task_id], references: [id], onDelete: Cascade, onUpdate: NoAction) | ||
|
|
||
| @@allow('all', true) | ||
| @@unique([from_task_id, to_task_id, link_type]) | ||
| @@index([link_type], map: "idx_task_links_type") | ||
| @@index([to_task_id], map: "idx_task_links_to") | ||
| @@index([from_task_id], map: "idx_task_links_from") | ||
| } | ||
|
|
||
| model audits { | ||
| id Int @id @default(autoincrement()) | ||
| timestamp Int @default(dbgenerated("unixepoch()")) | ||
| event_type String | ||
| object_type String | ||
| object_id String | ||
| field_name String? | ||
| old_value String? | ||
| new_value String? | ||
| actor String? | ||
|
|
||
| @@allow('all', true) |
There was a problem hiding this comment.
The ZenStack schema uses @@Allow('all', true) for all models, which allows unrestricted access to all operations on all data. This completely bypasses ZenStack's access control features and poses a serious security risk. Even for a POC, this should implement basic access control rules or at least document that this is intentionally disabled for development purposes only.
Summary
This PR introduces a web-based Kanban board POC built with Next.js 14, ZenStack v3 ORM, and Turso database, along with MCP server auto-sync capabilities for seamless AI agent integration.
Features
Web POC (apps/web-poc)
MCP Auto-Sync (packages/cli)
kaban turso-synccommandTurso Integration
kaban turso-syncChanges
New Files
apps/web-poc/- Complete Next.js web applicationpackages/cli/src/lib/mcp-auto-sync.ts- Auto-sync middlewarepackages/cli/src/commands/turso-sync.ts- Manual sync commanddocs/SYNC_ARCHITECTURE.md- Architecture documentationModified Files
packages/core/src/schemas.ts- Add Turso config fieldspackages/core/src/db/types.ts- Add syncUrl/syncInterval to DbConfigpackages/cli/src/commands/mcp.ts- Integrate auto-sync triggerspackages/cli/src/index.ts- Export new commandsDeployment
Web POC deployed to: https://kaban-web-poc.personal-261.workers.dev
Testing
Architecture
Commits
4785116- feat(core): add Turso sync configuration to schemas and types66e475d- feat(cli): integrate auto-sync triggers into MCP server1e9bfd4- feat(web-poc): add Cloudflare Turso client configuration703108e- docs: add Turso sync architecture documentation0448b22- chore(web-poc): update dependencies for ZenStack and Tursoaf0abd7- fix(web-poc): fix Cloudflare deployment configuration