Files
Keith Solomon 812e5c2f2a feature: Initial MVP
2026-04-05 16:20:39 -05:00

8.9 KiB
Raw Permalink Blame History

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:

  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 “dont 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