636 lines
8.9 KiB
Markdown
636 lines
8.9 KiB
Markdown
# 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
|