Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions aidd_docs/tasks/2026_08/2026_08_11_kanban-web-view/phase-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
status: done
---

# Instruction: Filesystem watcher adapter

## Architecture projection

> Tree of the final files. βœ… create Β· ✏️ modify Β· ❌ delete

```txt
kanban/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ domain/
β”‚ β”‚ └── ports/
β”‚ β”‚ └── βœ… task-document-watcher.ts
β”‚ └── infrastructure/
β”‚ └── filesystem/
β”‚ └── βœ… filesystem-task-document-watcher.ts
└── tests/
└── infrastructure/
└── βœ… filesystem-task-document-watcher.test.ts
```

## User Journey

```mermaid
flowchart TD
Start["Watcher starts on aidd_docs/"] --> Watch["fs.watch recursive"]
Watch --> Change["File .md created/modified/deleted"]
Change --> Debounce["Debounce 500ms"]
Debounce --> Rescan["Re-run findAll via repository"]
Rescan --> Callback["Emit new TaskGroup[] to onChange listener"]
Callback --> Watch
```

## Test Scope

```mermaid
---
title: Test scope
---
journey
section Setup
Create temp dir with aidd_docs/ and plan.md => ready: 5: system
section Happy path
Start watcher => onChange not called yet: 5: system
Write a new .md file => onChange fires with updated TaskGroup[]: 5: system
Modify frontmatter status => onChange fires with new status: 5: system
section Edge case - rapid changes
Write 3 files within 100ms => onChange fires once after debounce: 1: system
section Teardown
Stop watcher => no more callbacks, fs.watch closed: 5: system
```

## Tasks to do

### `1)` Define the watcher port

> Interface contract for watching task document changes.

1. Create `task-document-watcher.ts` in `domain/ports/`
2. Export `TaskDocumentWatcher` interface with `start(projectPath: string): void`, `stop(): void`, and `onChange(callback: (groups: TaskGroup[]) => void): void`

### `2)` Implement the filesystem watcher adapter

> Adapter that watches `aidd_docs/` and re-scans on changes.

1. Create `filesystem-task-document-watcher.ts` in `infrastructure/filesystem/`
2. Use `fs.watch` with `{ recursive: true }` on `<projectPath>/<docsDirectoryName>`
3. Filter for `.md` file events only
4. Debounce 500ms before re-scanning
5. On debounce trigger, call `taskDocumentRepository.findAll()` then `groupTaskDocumentsByDirectory()`, then invoke the onChange callback with the new TaskGroup[]
6. `stop()` closes the watcher and clears pending timers

### `3)` Test the watcher

> Integration test with real filesystem operations.

1. Create temp directory with `aidd_docs/tasks/` structure
2. Start watcher, write a file, assert onChange fires with correct data
3. Test debounce: multiple rapid writes produce a single callback
4. Test stop: no callbacks after stop

## Test acceptance criteria

| Task | Acceptance criteria |
| ---- | ---------------------------------------------------------------------------- |
| 1 | `TaskDocumentWatcher` interface exists with start, stop, onChange methods |
| 2 | Writing a .md file under the watched dir triggers onChange within 1s |
| 2 | Rapid successive writes produce a single onChange call after debounce |
| 2 | Calling stop() prevents further callbacks and releases the fs.watch handle |
| 3 | All tests pass with `vitest run` |
106 changes: 106 additions & 0 deletions aidd_docs/tasks/2026_08/2026_08_11_kanban-web-view/phase-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
status: done
---

# Instruction: HTTP server and SSE streaming

## Architecture projection

> Tree of the final files. βœ… create Β· ✏️ modify Β· ❌ delete

```txt
kanban/
β”œβ”€β”€ src/
β”‚ └── presentation/
β”‚ └── web/
β”‚ β”œβ”€β”€ βœ… http-server.ts
β”‚ └── βœ… sse-manager.ts
└── tests/
└── presentation/
└── βœ… http-server.test.ts
```

## User Journey

```mermaid
flowchart TD
Start["Server starts on port 3000"] --> Listen["HTTP listen"]
Listen --> Req{"Request path?"}
Req -->|"GET /"| Serve["Serve index.html"]
Req -->|"GET /styles.css"| CSS["Serve styles.css"]
Req -->|"GET /app.js"| JS["Serve app.js"]
Req -->|"GET /api/tasks"| REST["JSON: current TaskGroup[]"]
Req -->|"GET /events"| SSE["Open SSE connection"]
SSE --> Push["On watcher onChange => broadcast to all SSE clients"]
```

## Test Scope

```mermaid
---
title: Test scope
---
journey
section Setup
Start server on random port with mock data => listening: 5: system
section Happy path
GET / => 200 with HTML content-type: 5: system
GET /api/tasks => 200 with JSON array: 5: system
GET /events => 200 with text/event-stream, connection stays open: 5: system
Trigger data change => SSE client receives event with TaskGroup[] payload: 5: system
section Edge case - port taken
Start on occupied port => server picks next available port: 1: system
section Edge case - SSE disconnect
Client disconnects => server removes from broadcast list, no crash: 1: system
section Teardown
Close server => all connections dropped, port freed: 5: system
```

## Tasks to do

### `1)` Build the SSE manager

> Manage SSE client connections and broadcast events.

1. Create `sse-manager.ts` in `presentation/web/`
2. `addClient(res: ServerResponse)`: set SSE headers (`text/event-stream`, `no-cache`, `keep-alive`), push to client list, remove on `close` event
3. `broadcast(data: TaskGroup[])`: write `data: JSON\n\n` to every connected client
4. `closeAll()`: end every response, clear list

### `2)` Build the HTTP server

> Lightweight Node native HTTP server serving the frontend and API.

1. Create `http-server.ts` in `presentation/web/`
2. Export `KanbanWebServer` class taking constructor deps: `port`, `projectPath`, `docsDirectoryName`, `taskDocumentRepository`, `taskDocumentWatcher`
3. Route `GET /` => serve index.html (imported as string via tsup text loader)
4. Route `GET /styles.css` => serve CSS
5. Route `GET /app.js` => serve JS
6. Route `GET /api/tasks` => scan and return TaskGroup[] as JSON
7. Route `GET /events` => register SSE client via SseManager
8. On watcher onChange => `sseManager.broadcast(newGroups)`
9. `start()`: create http server, start watcher, listen. Print URL to output
10. `stop()`: stop watcher, close all SSE clients, close server

### `3)` Test the server

> Integration test with real HTTP requests.

1. Start server on port 0 (random)
2. Test GET / returns HTML
3. Test GET /api/tasks returns valid JSON
4. Test GET /events opens SSE stream
5. Test broadcast sends data to connected SSE client
6. Test server stops cleanly

## Test acceptance criteria

| Task | Acceptance criteria |
| ---- | -------------------------------------------------------------------------------- |
| 1 | SSE clients receive JSON-encoded TaskGroup[] on broadcast |
| 1 | Disconnected clients are removed without crashing the server |
| 2 | GET / returns 200 with content-type text/html |
| 2 | GET /api/tasks returns 200 with content-type application/json and a valid array |
| 2 | GET /events returns 200 with content-type text/event-stream |
| 2 | Server prints its URL to the output channel on start |
| 3 | All tests pass with `vitest run` |
140 changes: 140 additions & 0 deletions aidd_docs/tasks/2026_08/2026_08_11_kanban-web-view/phase-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
status: done
---

# Instruction: Frontend kanban board

## Architecture projection

> Tree of the final files. βœ… create Β· ✏️ modify Β· ❌ delete

```txt
kanban/
└── src/
└── presentation/
└── web/
└── frontend/
β”œβ”€β”€ βœ… index.html
β”œβ”€β”€ βœ… styles.css
└── βœ… app.js
```

## User Journey

```mermaid
flowchart TD
Open["Browser opens localhost:3000"] --> Load["Fetch GET /api/tasks"]
Load --> Render["Render 5 columns by ProgressStatus"]
Render --> Connect["Open EventSource /events"]
Connect --> Wait["Wait for SSE events"]
Wait --> Update["Receive new TaskGroup[]"]
Update --> Rerender["Re-render columns with new data"]
Rerender --> Wait
```

## Wireframe

```txt
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ (1) Header: "aidd kanban" Β· project path Β· connection status β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ (2) TODO β”‚ (3) IN β”‚ (4) DONE β”‚ (5) BLOCKEDβ”‚ (6) UNKNOWN β”‚
β”‚ β”‚ PROGRESS β”‚ β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚(7) Cardβ”‚ β”‚ β”‚ Card β”‚ β”‚ β”‚ Card β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ name β”‚ β”‚ β”‚ name β”‚ β”‚ β”‚ name β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ status β”‚ β”‚ β”‚ status β”‚ β”‚ β”‚ status β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 2/4 subβ”‚ β”‚ β”‚ 1/3 subβ”‚ β”‚ β”‚ 3/3 subβ”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ Card β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ (8) Footer: task count Β· last update timestamp β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

1. Header: title, project path, green/red dot for SSE connection state
2. TODO: cards with ProgressStatus `todo`
3. IN PROGRESS: cards with ProgressStatus `in-progress`
4. DONE: cards with ProgressStatus `done`
5. BLOCKED: cards with ProgressStatus `blocked`
6. UNKNOWN: cards with ProgressStatus `unknown`, column hidden when empty
7. Card: TaskGroup parent name, literal status badge, sub-document count with done ratio
8. Footer: total task count, last-updated timestamp

## Test Scope

```mermaid
---
title: Test scope
---
journey
section Setup
Start server with fixture data => browser opens: 5: browser
section Happy path
Page loads => 5 columns visible with cards in correct columns: 5: browser
Modify a plan.md frontmatter status => card moves to new column within 1s: 5: browser
section Edge case - empty project
No aidd_docs/ => board shows empty state message: 1: browser
section Edge case - SSE reconnect
Kill and restart server => board reconnects and refreshes: 1: browser
```

## Tasks to do

### `1)` Build index.html

> Single-page HTML shell with header, board container, footer.

1. Create `index.html` in `presentation/web/frontend/`
2. Semantic structure: header, main (board), footer
3. Link styles.css, defer app.js
4. Meta viewport for responsive layout

### `2)` Build styles.css

> Dark-theme-first CSS for the kanban board.

1. Create `styles.css` in `presentation/web/frontend/`
2. CSS custom properties for colors (dark theme default, `prefers-color-scheme: light` override)
3. Flexbox layout: header fixed top, columns flex-row equal-width, cards as column items
4. Card styles: border, padding, name bold, status as colored badge, sub-doc count dimmed
5. Column header: uppercase label, item count badge
6. Connection indicator: green/red dot in header
7. Responsive: columns stack vertically below 640px

### `3)` Build app.js

> Vanilla JS: fetch initial data, connect SSE, render and update the board.

1. Create `app.js` in `presentation/web/frontend/`
2. On load: `fetch('/api/tasks')`, render columns
3. `renderBoard(taskGroups)`: clear board, for each ProgressStatus create column, place cards by parent's progressStatus
4. Card rendering: parent name, status badge (colored by progressStatus), sub-document progress bar ("N/M done")
5. `EventSource('/events')`: on message, parse JSON, call `renderBoard()`
6. Connection status: update header dot on EventSource open/error events
7. Footer: update task count and "Last updated: HH:MM:SS"
8. Hide UNKNOWN column when it has zero cards

### `4)` Manual browser verification

> Confirm the board renders and updates live.

1. Start the server with the framework's own `aidd_docs/tasks/`
2. Verify columns display correctly in the browser
3. Edit a `plan.md` frontmatter status, confirm the card moves within 1s
4. Check dark and light themes

## Test acceptance criteria

| Task | Acceptance criteria |
| ---- | ------------------------------------------------------------------------------------ |
| 1 | index.html loads without console errors in a browser |
| 2 | Cards are visually grouped in columns, readable on a 1280px-wide screen |
| 2 | Light theme activates when OS preference is light |
| 3 | On page load, columns reflect the current TaskGroup data from /api/tasks |
| 3 | When a .md file is modified, the board updates within 1s without page reload |
| 3 | SSE disconnect shows a red indicator; reconnect restores the green dot |
| 3 | UNKNOWN column is hidden when no task has unknown progress status |
| 4 | Visual check passes on the framework's own aidd_docs/tasks/ data |
Loading