Synaplan
Open-source AI platform for companies: self-hosted AI agents, GDPR-compliant chatbot widget, RAG knowledge base, WhatsApp and Outlook AI, MCP, multi-model routing. Docker, Kubernetes or hosted in Germ
翻訳待ち
No updates detected yet. GitHub releases will appear here.
Loading the full README. The saved excerpt is shown below.
A mini project-management app for one team. Organise work into spaces → folders → lists, manage tasks on a kanban board or in a list view, and control who can see what. A built-in user switcher shows the difference.
Kanban board — Sprint 14, viewed as Alice (admin)
Task drawer — status, priority, multiple assignees, due date, subtasks
List view — sortable columns, assignee filter, pagination
Permissions — Bob (member) hits a clean 403; nothing forbidden leaks
🎥 Demo video: Watch on Loom (about 4 min)Read the full README ↗A mini project-management app for one team. Organise work into spaces → folders → lists, manage tasks on a kanban board or in a list view, and control who can see what. A built-in user switcher shows the difference.
🔗 Deployed link: flowboard-delta-ruddy.vercel.app
🎥 Demo video: Watch on Loom (about 4 min)
Stack: React 18 · TypeScript (strict) · Vite 5 · Tailwind CSS 3 · Zustand · dnd-kit · Headless UI · Vitest + Testing Library · Playwright. There's no backend: a typed Zustand store, seeded with demo data, plays the role of the API and database.
Stretch goals attempted (2): ① optimistic drag-and-drop with rollback on failure · ② client-side search on task title and description (⌘K or /)
Requires Node.js 20+ (developed on Node 22).
npm install
npm run dev # → http://localhost:5173
Nothing to configure: no .env, no API keys, no database. The app opens as Alice (admin). To start over, use user menu → Demo controls → Reset demo data. More commands, the pre-commit hook and editor setup are in 10. Development.
Drag-and-drop and rollback: drag a card to another column. It moves instantly, shows a spinner while "saving", and stays there after a refresh. Then turn on user menu → Demo controls → Simulate save failures and drag again: the card moves, then snaps back with a "Save failed" toast.
flowchart TD
subgraph UI["React components (Tailwind only)"]
Views["Sidebar · TopBar/UserSwitcher · ListScreen<br/>BoardView · ListView · TaskDrawer<br/>SearchPalette · dialogs · Toaster"]
end
subgraph Store["Client data layer"]
AS["appStore (Zustand)<br/>data · currentUserId · loadingListId · pendingTaskIds"]
TR["transport.ts<br/>fake latency / failure"]
PE["persistence.ts<br/>localStorage"]
end
subgraph Domain["Pure domain (framework-free, unit tested)"]
MUT["tasks.ts · containers.ts · statuses.ts<br/>(state, actor, input) → Result"]
SEL["selectors.ts · tree.ts<br/>board · list page · detail · search"]
PM["permissions.ts<br/>resolveAccess · guards"]
end
Views -- "actions.*() / useAppStore(selector)" --> AS
AS -- commit --> MUT
AS -- read --> SEL
MUT --> PM
SEL --> PM
AS -. "await save" .-> TR
AS -. subscribe .-> PE
src/domain/) holds the rules. Every change is a pure function that takes the current data and returns new data or an error. Every read is a selector that takes the current user and returns only what that user may see. No React and no store library, so it's easy to test.src/store/) is a thin Zustand wrapper. Each action calls a domain function; on success it saves the new state, on error it shows a toast automatically, so components never need their own error handling.src/components/) read through selectors and write through useActions(). Screen-level components (Sidebar, ListScreen, TaskDrawer) connect to the store and pass data and callbacks down. The shared ui/ components and the sidebar's tree, rows, header and archive list take props only; an ESLint rule stops them importing the store.What happens when you drag a card:
BoardView gets the drop from dnd-kit and calls actions.moveTask({ taskId, toStatusId, toIndex }).moveTask checks permissions (both lists, if the task changes lists), validates the status and re-numbers the column's positions.simulateSave(), a fake network call in transport.ts.persistence.ts writes the new state to localStorage.Switching users works the same way: only currentUserId changes, and every selector takes the user as an argument, so the tree, board, drawer and search update on the next render.
Components don't subscribe to the whole data set. useDataWith('tasks', 'statuses') returns just those parts (plus the users, containers and grants every permission check needs), and the domain read functions are typed to accept exactly that (DataWith<...>). A component re-renders only when a part it named is replaced, so a new comment doesn't rebuild the sidebar tree or the board. The task drawer and the search palette read data only while open. src/test/subscriptions.test.tsx fails if this regresses.
The brief says "We evaluate your judgment", so here's the reasoning:
This is the beginning of the README. Read the rest at the original. Read the full README ↗