8.9 KiB
8.9 KiB
IronKanban — Technical Blueprint (Dev Spec)
0. Scope
MVP implementation of:
- Git-backed markdown project/task system
- Kanban board UI
- Drag & drop between columns
- Real-time-ish updates (polling → SSE-ready)
- PHP backend + vanilla JS frontend
- No framework (structured, but lightweight)
1. System Architecture
1.1 High-level flow
Filesystem (markdown + git)
↑ ↓
Repositories (PHP)
↑ ↓
Services (business logic)
↑ ↓
API Endpoints (public/api/*.php)
↑ ↓
Frontend (vanilla JS + SortableJS)
1.2 Request lifecycle (example: move task)
Drag card →
JS computes new position →
POST /api/move-task.php →
TaskRepository loads file →
BoardService updates column/order →
TaskRepository writes file →
GitService commits →
RevisionService updates revision →
Response JSON →
UI updates (already optimistic)
2. Filesystem Contract
2.1 Root config
const PROJECT_ROOT = '/data/projects';
All file access must resolve within this root.
2.2 Project layout
/data/projects/{projectSlug}/
├── index.md
├── board.md
├── notes/
│ └── *.md
├── tasks/
│ └── *.md
3. Markdown Contract
3.1 Front matter parser contract
class FrontMatterDocument {
public array $meta;
public string $body;
}
3.2 Required keys by type
Project (index.md)
type: project
id: string
title: string
created: ISO8601
updated: ISO8601
Board (board.md)
type: board
project_id: string
columns:
- id: string
label: string
order: int
updated: ISO8601
Task (tasks/*.md)
type: task
id: string
title: string
project_id: string
column: string
order: int
completed: bool
priority: string
is_active: bool
created: ISO8601
updated: ISO8601
Optional (must be preserved if present):
status: string|null
status_note: string|null
assignee: string|null
tags: array
section: string # legacy
Note (notes/*.md)
type: note
id: string
title: string
project_id: string
created: ISO8601
updated: ISO8601
4. Domain Models
4.1 Project
class Project {
public string $id;
public string $title;
public string $path;
}
4.2 Task
class Task {
public string $id;
public string $title;
public string $projectId;
public string $column;
public int $order;
public bool $completed;
public string $priority;
public bool $isActive;
public array $meta;
public string $body;
}
4.3 Board
class Board {
public string $projectId;
/** @var Column[] */
public array $columns;
}
4.4 Column
class Column {
public string $id;
public string $label;
public int $order;
}
4.5 Note
class Note {
public string $id;
public string $title;
public string $body;
}
5. Repositories
5.1 ProjectRepository
class ProjectRepository {
public function getAll(): array;
public function get(string $slug): Project;
}
5.2 TaskRepository
class TaskRepository {
public function getAll(string $projectId): array;
public function get(string $projectId, string $taskId): Task;
public function save(Task $task): void;
public function create(array $data): Task;
public function delete(string $projectId, string $taskId): void;
}
Responsibilities
- read/write markdown
- preserve unknown front matter keys
- handle file paths
- ensure atomic writes
5.3 BoardRepository
class BoardRepository {
public function get(string $projectId): Board;
public function save(Board $board): void;
}
5.4 NoteRepository
class NoteRepository {
public function getAll(string $projectId): array;
public function save(Note $note): void;
}
6. Services
6.1 BoardService
class BoardService {
public function getBoardState(string $projectId): array;
public function moveTask(
string $projectId,
string $taskId,
string $column,
int $newOrder
): void;
}
6.2 TaskOrderingService
class TaskOrderingService {
public function computeOrder(array $tasks, int $targetIndex): int;
public function rebalance(array $tasks): array;
}
Rule
- Use gaps: 100, 200, 300
- Insert = midpoint
- Rebalance only if gap < threshold
6.3 GitService
class GitService {
public function commit(string $message): void;
}
Implementation (MVP)
git add .
git commit -m "..."
Future
- scoped add
- branch support
6.4 RevisionService
class RevisionService {
public function get(string $projectId): string;
public function bump(string $projectId): void;
}
Storage
/data/projects/{project}/.revision
6.5 FileLock
class FileLock {
public static function run(string $path, callable $callback);
}
Uses flock().
7. API Specification
All endpoints return JSON.
7.1 GET /api/board-state.php
Query
?project=personal-projects
Response
{
"project": {},
"board": {},
"columns": [],
"tasks": [],
"notes": [],
"revision": "timestamp"
}
7.2 POST /api/move-task.php
Input
{
"projectId": "personal-projects",
"taskId": "task-123",
"column": "doing",
"index": 2
}
Flow
load tasks in column →
compute order →
update task →
save →
git commit →
revision bump
7.3 POST /api/create-task.php
{
"projectId": "...",
"title": "...",
"column": "backlog",
"body": ""
}
7.4 POST /api/save-task.php
Full update:
{
"projectId": "...",
"taskId": "...",
"title": "...",
"body": "...",
"meta": {}
}
7.5 POST /api/delete-task.php
{
"projectId": "...",
"taskId": "..."
}
7.6 POST /api/create-column.php
{
"projectId": "...",
"label": "Blocked"
}
7.7 POST /api/update-board.php
Reorder / rename columns.
7.8 POST /api/save-note.php
7.9 GET /api/events.php (Phase 2)
SSE stream:
event: boardUpdated
data: { "revision": "..." }
8. Frontend Spec
8.1 Stack
- Vanilla JS
- SortableJS (drag/drop)
- Server-rendered HTML + JS hydration
8.2 State model
const state = {
projectId: null,
columns: [],
tasks: [],
revision: null
};
8.3 Drag/drop flow
onEnd →
get taskId
get new column
get index
compute optimistic order
update DOM
POST move-task
if fail → revert
8.4 Polling loop (MVP)
setInterval(async () => {
const res = await fetch('/api/board-state?project=...');
if (res.revision !== state.revision) {
reloadBoard();
}
}, 5000);
8.5 SSE (Phase 2)
const evt = new EventSource('/api/events.php?project=...');
evt.onmessage = () => reloadBoard();
9. Migration Logic
9.1 Missing board.md
scan tasks →
collect unique section values →
generate columns →
write board.md
9.2 Missing column
if (!column && section) {
column = slugify(section);
}
9.3 Missing order
Assign:
order = index * 100;
10. File Write Rules
10.1 Atomic write
write temp file →
fsync →
rename →
unlock
10.2 Preserve unknown metadata
Never discard keys.
10.3 Encoding
UTF-8 only.
11. Error Handling
11.1 API response format
{
"success": false,
"error": "message"
}
11.2 Conflict detection
If revision mismatch:
{
"success": false,
"error": "conflict"
}
12. Security
- No path traversal
- Sanitize projectId
- Whitelist file extensions
- Escape shell commands
- Run git in repo root only
13. Performance Considerations
- Cache parsed board in memory per request
- Avoid re-reading unchanged files
- Lazy-load notes if needed
- Debounce save operations (frontend)
14. MVP Milestones
Phase 1
- File parsing
- Board rendering
- Basic UI
Phase 2
- Drag/drop
- Save APIs
Phase 3
- Task CRUD
- Column CRUD
Phase 4
- Git integration
- Revision tracking
Phase 5
- Polling updates
Phase 6 (optional)
- SSE
15. Out-of-Scope (MVP)
- Auth system
- Multi-repo support
- Comments
- Subtasks
- Attachments
- Permissions
- Websockets
16. Build Order (Practical)
I suggest this order:
- Markdown parser + writer
- TaskRepository
- BoardRepository
- BoardService (read-only)
- Render board (no JS yet)
- Add SortableJS
- Move-task endpoint
- Create/edit task
- GitService
- Revision + polling
17. Key Architectural Decisions (Locked)
These are your “don’t rethink this at 2am” decisions:
- ✅ tasks remain flat files in /tasks
- ✅ kanban state = column in front matter
- ✅ board config = board.md
- ✅ ordering = sparse integers
- ✅ git is first-class
- ✅ PHP handles everything server-side
- ✅ SortableJS is allowed
- ❌ no folder-per-column model
- ❌ no heavy framework