✨feature: Initial MVP
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
# 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
|
||||
|
||||
```plain
|
||||
Filesystem (markdown + git)
|
||||
↑ ↓
|
||||
Repositories (PHP)
|
||||
↑ ↓
|
||||
Services (business logic)
|
||||
↑ ↓
|
||||
API Endpoints (public/api/*.php)
|
||||
↑ ↓
|
||||
Frontend (vanilla JS + SortableJS)
|
||||
```
|
||||
|
||||
### 1.2 Request lifecycle (example: move task)
|
||||
|
||||
```plain
|
||||
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
|
||||
|
||||
```php
|
||||
const PROJECT_ROOT = '/data/projects';
|
||||
```
|
||||
|
||||
All file access must resolve within this root.
|
||||
|
||||
### 2.2 Project layout
|
||||
|
||||
```plain
|
||||
/data/projects/{projectSlug}/
|
||||
├── index.md
|
||||
├── board.md
|
||||
├── notes/
|
||||
│ └── *.md
|
||||
├── tasks/
|
||||
│ └── *.md
|
||||
```
|
||||
|
||||
## 3. Markdown Contract
|
||||
|
||||
### 3.1 Front matter parser contract
|
||||
|
||||
```php
|
||||
class FrontMatterDocument {
|
||||
public array $meta;
|
||||
public string $body;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Required keys by type
|
||||
|
||||
#### Project (`index.md`)
|
||||
|
||||
```yaml
|
||||
type: project
|
||||
id: string
|
||||
title: string
|
||||
created: ISO8601
|
||||
updated: ISO8601
|
||||
```
|
||||
|
||||
#### Board (`board.md`)
|
||||
|
||||
```yaml
|
||||
type: board
|
||||
project_id: string
|
||||
columns:
|
||||
- id: string
|
||||
label: string
|
||||
order: int
|
||||
updated: ISO8601
|
||||
```
|
||||
|
||||
#### Task (`tasks/*.md`)
|
||||
|
||||
```yaml
|
||||
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):
|
||||
|
||||
```yaml
|
||||
status: string|null
|
||||
status_note: string|null
|
||||
assignee: string|null
|
||||
tags: array
|
||||
section: string # legacy
|
||||
```
|
||||
|
||||
#### Note (`notes/*.md`)
|
||||
|
||||
```yaml
|
||||
type: note
|
||||
id: string
|
||||
title: string
|
||||
project_id: string
|
||||
created: ISO8601
|
||||
updated: ISO8601
|
||||
```
|
||||
|
||||
## 4. Domain Models
|
||||
|
||||
### 4.1 Project
|
||||
|
||||
```php
|
||||
class Project {
|
||||
public string $id;
|
||||
public string $title;
|
||||
public string $path;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Task
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
```php
|
||||
class Board {
|
||||
public string $projectId;
|
||||
|
||||
/** @var Column[] */
|
||||
public array $columns;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Column
|
||||
|
||||
```php
|
||||
class Column {
|
||||
public string $id;
|
||||
public string $label;
|
||||
public int $order;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Note
|
||||
|
||||
```php
|
||||
class Note {
|
||||
public string $id;
|
||||
public string $title;
|
||||
public string $body;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Repositories
|
||||
|
||||
### 5.1 ProjectRepository
|
||||
|
||||
```php
|
||||
class ProjectRepository {
|
||||
public function getAll(): array;
|
||||
public function get(string $slug): Project;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 TaskRepository
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
```php
|
||||
class BoardRepository {
|
||||
public function get(string $projectId): Board;
|
||||
public function save(Board $board): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 NoteRepository
|
||||
|
||||
```php
|
||||
class NoteRepository {
|
||||
public function getAll(string $projectId): array;
|
||||
public function save(Note $note): void;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Services
|
||||
|
||||
### 6.1 BoardService
|
||||
|
||||
```php
|
||||
class BoardService {
|
||||
public function getBoardState(string $projectId): array;
|
||||
|
||||
public function moveTask(
|
||||
string $projectId,
|
||||
string $taskId,
|
||||
string $column,
|
||||
int $newOrder
|
||||
): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 TaskOrderingService
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
```php
|
||||
class GitService {
|
||||
public function commit(string $message): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### Implementation (MVP)
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "..."
|
||||
```
|
||||
|
||||
#### Future
|
||||
|
||||
- scoped add
|
||||
- branch support
|
||||
|
||||
### 6.4 RevisionService
|
||||
|
||||
```php
|
||||
class RevisionService {
|
||||
public function get(string $projectId): string;
|
||||
public function bump(string $projectId): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### Storage
|
||||
|
||||
```plain
|
||||
/data/projects/{project}/.revision
|
||||
```
|
||||
|
||||
### 6.5 FileLock
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"project": {},
|
||||
"board": {},
|
||||
"columns": [],
|
||||
"tasks": [],
|
||||
"notes": [],
|
||||
"revision": "timestamp"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 POST `/api/move-task.php`
|
||||
|
||||
#### Input
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "personal-projects",
|
||||
"taskId": "task-123",
|
||||
"column": "doing",
|
||||
"index": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### Flow
|
||||
|
||||
```plain
|
||||
load tasks in column →
|
||||
compute order →
|
||||
update task →
|
||||
save →
|
||||
git commit →
|
||||
revision bump
|
||||
```
|
||||
|
||||
### 7.3 POST `/api/create-task.php`
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "...",
|
||||
"title": "...",
|
||||
"column": "backlog",
|
||||
"body": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 POST `/api/save-task.php`
|
||||
|
||||
Full update:
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "...",
|
||||
"taskId": "...",
|
||||
"title": "...",
|
||||
"body": "...",
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.5 POST `/api/delete-task.php`
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "...",
|
||||
"taskId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 7.6 POST `/api/create-column.php`
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```plain
|
||||
event: boardUpdated
|
||||
data: { "revision": "..." }
|
||||
```
|
||||
|
||||
## 8. Frontend Spec
|
||||
|
||||
### 8.1 Stack
|
||||
|
||||
- Vanilla JS
|
||||
- SortableJS (drag/drop)
|
||||
- Server-rendered HTML + JS hydration
|
||||
|
||||
### 8.2 State model
|
||||
|
||||
```javaScript
|
||||
const state = {
|
||||
projectId: null,
|
||||
columns: [],
|
||||
tasks: [],
|
||||
revision: null
|
||||
};
|
||||
```
|
||||
|
||||
### 8.3 Drag/drop flow
|
||||
|
||||
```javaScript
|
||||
onEnd →
|
||||
get taskId
|
||||
get new column
|
||||
get index
|
||||
compute optimistic order
|
||||
update DOM
|
||||
POST move-task
|
||||
if fail → revert
|
||||
```
|
||||
|
||||
### 8.4 Polling loop (MVP)
|
||||
|
||||
```javaScript
|
||||
setInterval(async () => {
|
||||
const res = await fetch('/api/board-state?project=...');
|
||||
if (res.revision !== state.revision) {
|
||||
reloadBoard();
|
||||
}
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
### 8.5 SSE (Phase 2)
|
||||
|
||||
```javaScript
|
||||
const evt = new EventSource('/api/events.php?project=...');
|
||||
evt.onmessage = () => reloadBoard();
|
||||
```
|
||||
|
||||
## 9. Migration Logic
|
||||
|
||||
### 9.1 Missing board.md
|
||||
|
||||
```plain
|
||||
scan tasks →
|
||||
collect unique section values →
|
||||
generate columns →
|
||||
write board.md
|
||||
```
|
||||
|
||||
### 9.2 Missing column
|
||||
|
||||
```php
|
||||
if (!column && section) {
|
||||
column = slugify(section);
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 Missing order
|
||||
|
||||
Assign:
|
||||
|
||||
`order = index * 100;`
|
||||
|
||||
## 10. File Write Rules
|
||||
|
||||
### 10.1 Atomic write
|
||||
|
||||
```plain
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "message"
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 Conflict detection
|
||||
|
||||
If revision mismatch:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
1. Markdown parser + writer
|
||||
2. TaskRepository
|
||||
3. BoardRepository
|
||||
4. BoardService (read-only)
|
||||
5. Render board (no JS yet)
|
||||
6. Add SortableJS
|
||||
7. Move-task endpoint
|
||||
8. Create/edit task
|
||||
9. GitService
|
||||
10. 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
|
||||
Reference in New Issue
Block a user