✨feature: Initial MVP
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# IronKanban
|
||||
|
||||
I'm currently using this project <https://github.com/OlaProeis/ironPad> in a custom docker container I wrapped it in. While it works, and I like the flat-file git-backed nature of it, I'm not thrilled with the interaction pattern. I would like to keep the markdown powered (using the same format for the notes and projects if at all possible) git repo aspect of it, but build it out into a kanban-style interface. I would also like to move away from the stack IronPad is using, and move to something I'm more comfortable with.
|
||||
|
||||
I would prefer PHP with vanilla javascript as needed. That said, I would like to have the abilty to drag and drop cards between columns, and have the interface update in real(ish) time as changes are made. I would also like to have the ability to create new columns and cards, and have those changes reflected in the underlying markdown files. Each Project is currently built as a folder that is laid out like this:
|
||||
|
||||
```plain
|
||||
project-name/
|
||||
├── index.md
|
||||
├── notes/
|
||||
│ └── 20260314-154222.md
|
||||
├── tasks/
|
||||
│ ├── task-20260227-124827.md
|
||||
│ ├── task-20260227-124901.md
|
||||
│ ├── task-20260227-165334.md
|
||||
│ └── task-20260314-154158.md
|
||||
```
|
||||
|
||||
The index.md file contains the project description and any relevant information about the project. The optional notes folder contains any notes related to the project, and the tasks folder contains the individual tasks for the project.
|
||||
|
||||
The UI should be dark mode by default, and should be responsive. The columns should be able to be reordered, and the tasks within the columns should also be able to be reordered. The tasks should also be able to be moved between columns. The UI should also have a way to create new columns and tasks, and to edit existing ones. The UI should also have a way to delete columns and tasks, and to move tasks to a "trash" column before they are permanently deleted.
|
||||
|
||||
See `dev-spec.md` for more detailed specifications on the system architecture, request lifecycle, and filesystem contract.
|
||||
@@ -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