| name | kanban-dashboard |
| description | {{AGENT_NAME}} kanban board UI โ Flask dashboard server for visual task management |
Kanban Dashboard Skill
Kanban Dashboard โ Flask Server (Primary)
Flask server๊ฐ kanban board๋ฅผ ๋ ๋๋ง. SSE ์ค์๊ฐ ์
๋ฐ์ดํธ + ๋๋๊ทธ๋๋กญ + ๋ชจ๋ฐ์ผ ๋์.
URL: http://macmini:8765/kanban
๋ ์ด์์
- ๋ณด๋ ํญ: All / default / content / integrations โ ํญ ํด๋ฆญ์ผ๋ก ๋ณด๋ ํํฐ
- ์ปฌ๋ผ: To Do | Ready | In Progress | Blocked | Completed โ ์ํ๋ณ ํ ์ค
- ์นด๋ ํ์ (3์ค):
- Title: ์ ๋ชฉ (60์ truncation, hover ์ ์ ์ฒด ํ์)
- Summary: body์์
## Topic ์ฒซ ๋ฌธ๋จ ์ถ์ถํ 1์ค ์์ฝ (.card-summary, 2์ค clamp)
- Meta row 1: Priority badge + Category badge (์ปฌ๋ฌ + ์ด๋ชจ์ง: draft-trend=๐#5bc0eb, draft-conversation=๐ฌ#aa66ff, draft-seo=๐#00c853 ๋ฑ) + Board + Worker + Created + Assignee
- Meta row 3 (if draft path exists): Draft file link (๐, ๋ณด๋ผ์
obsidian://open ๋งํฌ) โ _extract_draft_path()๋ก body ## Draft ํ์ผ ์์น ํ์ฑ, _obsidian_url()๋ก URL ๋ณํ
- ์นด๋ ์์ธ ๋ชจ๋ฌ: ์นด๋ ํด๋ฆญ ์ ๋ชจ๋ฌ ์คํ. 5๊ฐ ํญ:
- Description (๊ธฐ๋ณธ): task body (๋ด์ฉ/์ค๋ช
) ํ์ + ๐ Open in Obsidian ๋ฒํผ (body์
## Draft ํ์ผ ์์น ๊ฒฝ๋ก๊ฐ ์์ผ๋ฉด ๋ ๋๋ง) + โ๏ธ Edit ๋ฒํผ
- Events: ์์ฑ, ์๋ฃ, ๋ธ๋ก, ํด๋ ์ ๋ฑ ์ด๋ฒคํธ ํ์คํ ๋ฆฌ
- Result: task result ํ๋ (์์
๊ฒฐ๊ณผ๋ฌผ)
- Info: ๋ชจ๋ ๋ฉํ ํ๋ (trigger_source, tenant, claim_lock, worker_pid, heartbeat ๋ฑ)
- Escape ํค ๋๋ overlay ํด๋ฆญ์ผ๋ก ๋ซ๊ธฐ
- ๋ฐ์ดํฐ ์ถ์ฒ:
GET /kanban/api/task/<id>
- Body ์๋ํฐ: Description ํญ์์ Edit ๋ฒํผ โ title input + body textarea + Save/Cancel ๋ฒํผ์ผ๋ก ์ ํ. ์ ์ฅ ์
POST /kanban/api/edit ํธ์ถ. ์ ์ฅ ํ SSE ๋ธ๋ก๋์บ์คํธ๋ก ๋ณด๋ ์๋ก๊ณ ์นจ.
- ์ค์๊ฐ: SSE ์คํธ๋ฆผ ์ฐ๊ฒฐ โ ์นด๋ ์ก์
์ ์ฆ์ ํ๋ฉด ๋ฐ์ (์ด๋ก ์ ์ผ๋ก ์ฐ๊ฒฐ ์ํ ํ์)
- ๋๋๊ทธ๋๋กญ: ์นด๋๋ฅผ ๋ค๋ฅธ ์ปฌ๋ผ์ผ๋ก ๋๋๊ทธํ๋ฉด ์ํ ์๋ ๋ณ๊ฒฝ
- ๋ชจ๋ฐ์ผ: ํฐ์น ์คํฌ๋กค, ์์ ํ๋ฉด์์ ์ปฌ๋ผ ๋๋น ์ถ์
LaunchAgent (Self-Healing, Auto-Restart)
~/Library/LaunchAgents/ai.{{AGENT_NAME_LOWER}}.kanban-dashboard.plist
KeepAlive: SuccessfulExit=false โ ํ๋ก์ธ์ค ์ฃฝ์ผ๋ฉด ์๋ ์ฌ์์
- MacMini ์ฌ๋ถํ
ํด๋ ์๋ ์คํ
- ๋ก๊ทธ:
~/.{{AGENT_NAME_LOWER}}/P6-prefrontal/logs/kanban-server.log
๊ด๋ฆฌ ๋ช
๋ น
launchctl list | grep kanban
launchctl start ai.{{AGENT_NAME_LOWER}}.kanban-dashboard
launchctl stop ai.{{AGENT_NAME_LOWER}}.kanban-dashboard
tail -f ~/.{{AGENT_NAME_LOWER}}/P6-prefrontal/logs/kanban-server.log
์๋ํฌ์ธํธ
| Method | Path | Description |
|---|
| GET | /kanban | Kanban board HTML (SSE ์ค์๊ฐ ์
๋ฐ์ดํธ) |
| GET | /kanban/api/board | JSON board state (tasks grouped by status) |
| GET | /kanban/api/task/<task_id> | Full task detail (body + events) โ ๋ชจ๋ฌ ๋ฐ์ดํฐ ์์ค |
| GET | /kanban/api/events | Recent events (minutes query param) |
| GET | /kanban/api/stream | SSE ์ค์๊ฐ ์คํธ๋ฆผ |
| POST | /kanban/api/complete | task complete |
| POST | /kanban/api/claim | task claim |
| POST | /kanban/api/block | task block |
| POST | /kanban/api/unblock | task unblock |
| POST | /kanban/api/create | task create |
| POST | /kanban/api/delete | task delete |
| POST | /kanban/api/dispatch | task dispatch (spawn worker) |
| POST | /kanban/api/update_status | task status ๋ณ๊ฒฝ (๋๋๊ทธ๋๋กญ) |
| POST | /kanban/api/edit | task title/body ์์ (form: task_id, title, body) |
Body Parser Helpers (server-side)
kanban_dashboard_server.py์ ๋ด์ฅ๋ body ํ์ฑ ํจ์๋ค:
| ํจ์ | ์ฉ๋ |
|---|
_extract_summary(body) | ## Topic ์ฒซ ๋ฌธ๋จ ์ถ์ถ, bold/newline ์ ๊ฑฐ, 180์ truncation |
_extract_source(body) | ## Content Source ์น์
์์ source + score ํ์ฑ |
_categorize(title) | ์ ๋ชฉ [tag] prefix ์ถ์ถ (์: draft-trend, draft-conversation) |
_cat_color(cat) | ์นดํ
๊ณ ๋ฆฌ โ ์ปฌ๋ฌ ๋งคํ (trend=#5bc0eb, conversation=#aa66ff, seo=#00c853) |
_cat_emoji(cat) | ์นดํ
๊ณ ๋ฆฌ โ ์ด๋ชจ์ง ๋งคํ (trend=๐, conversation=๐ฌ, seo=๐) |
_extract_draft_path(body) | ## Draft ํ์ผ ์์น ์น์
์์ vault draft ์ ๋๊ฒฝ๋ก ์ถ์ถ |
_obsidian_url(vault_path) | ์ ๋๊ฒฝ๋ก โ obsidian://open?vault={{AGENT_NAME}}&file=... URL ๋ณํ (urllib.parse.quote๋ก ์ธ์ฝ๋ฉ) |
Obsidian Vault Integration
์นธ๋ฐ task body์ ## Draft ํ์ผ ์์น ์น์
์ผ๋ก vault ๊ฒฝ๋ก๊ฐ ๊ธฐ๋ก๋ ๊ฒฝ์ฐ, ๋์๋ณด๋์์ ์ง์ Obsidian์ผ๋ก ์ด๋ ๊ฐ๋ฅ:
Card-level
.card-draft-link: <a> ํ๊ทธ๋ก vault ํ์ผ๋ช
ํ์, ํด๋ฆญ ์ obsidian://open URL ์คํ
.card-draft: ๊ฒฝ๋ก๋ ์์ผ๋ URL ๋ณํ ์คํจ ์ ํ
์คํธ๋ก๋ง ํ์
- CSS: ๋ณด๋ผ์(#8b5cf6) dotted underline, hover ์ solid๋ก ๋ณ๊ฒฝ,
event.stopPropagation()์ผ๋ก ์นด๋ ํด๋ฆญ ์ด๋ฒคํธ ์ถฉ๋ ๋ฐฉ์ง
Modal-level
- Description ํญ ํ๋จ์ ๐ Open in Obsidian ๋ฒํผ (JS๋ก body์์
## Draft ํ์ผ ์์น regex ๋งค์นญ ํ ๋์ ์์ฑ)
- ๋ฒํผ:
background:#8b5cf6, ํฐ์ ํ
์คํธ, 6px radius
- URI scheme:
obsidian://open?vault={{AGENT_NAME}}&file={encodeURIComponent(relative_path)}
- draft ๊ฒฝ๋ก๊ฐ ์์ผ๋ฉด ๋ฒํผ ๋ฏธํ์
File path convention
- ์ ๋๊ฒฝ๋ก:
~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/YYYY-MM-slug.md
- ์๋๊ฒฝ๋ก (URL์ฉ):
P2-hippocampus/memories/insights/YYYY-MM-slug.md
- vault name:
{{AGENT_NAME}} (๊ณ ์ โ ~/.{{AGENT_NAME_LOWER}} ๋๋ ํ ๋ฆฌ๋ช
)
- Obsidian URL ๋์ ์กฐ๊ฑด: macOS์์ Obsidian์ด ์ค์น๋์ด ์์ด์ผ ํจ. Safari/Chrome/Firefox์์
obsidian:// scheme ํธ๋ค๋ฌ ๋ฑ๋ก ํ์.
Board Maintenance
Kanban ๋ณด๋ ์ ๋ฆฌ ํ๋กํ ์ฝ โ content pipeline์์ ์์ฑ๋ draft-trend task๊ฐ ์์ด๋ฉด:
- ์ค๋ณต ํ์ธ: ๊ฐ์ ์ฃผ์ ์ task๊ฐ ์ฌ๋ฌ ๊ฐ๋ฉด body๊ฐ ๋ ์์ฑ๋ ์ชฝ ์ ์ง, ๋๋จธ์ง ์ญ์
- Completed ์ ๋ฆฌ: ์๋ฃ๋ task๋
POST /kanban/api/delete๋ก ์ ๊ฑฐ (board clutter ๋ฐฉ์ง)
- Stale todo ์ ๋ฆฌ: assignee ์๊ณ 2์ฃผ ์ด์ ๋ฐฉ์น๋ todo๋ ์ญ์ ๋๋ ์ฌํ ๋น
- ์ต์ข
๊ฒ์ฆ:
GET /kanban/api/board๋ก ์ต์ข
์ํ ํ์ธ
์ ๋ฆฌ ๊ธฐ์ค: [draft-trend] task๋ ๊ฐ์ GitHub repo/์ฃผ์ ๋ช
์ด๋ฉด ์ค๋ณต์ผ๋ก ๊ฐ์ฃผ. assignee=None + created_at 14์ผ ์ด์์ด๋ฉด stale.
ํ์ผ
- Server script:
~/.{{AGENT_NAME_LOWER}}/P4-cortex/scripts/kanban_dashboard_server.py
- LaunchAgent plist:
~/Library/LaunchAgents/ai.{{AGENT_NAME_LOWER}}.kanban-dashboard.plist
Pitfalls
get_tasks() in server uses its own init_db() โ task table schema must match {{AGENT_NAME_LOWER}}_tasks.db (both use same board column). If schema mismatches, board returns empty.
- Access from outside:
http://macmini:8765/kanban (same network). MacMini hostname or IP ์ฌ์ฉ.
- f-string escape bug: Python f-string์์
{{var}} โ literal {var} ์ถ๋ ฅ. Python ๋ณ์ ์ฌ์ฉ์ {var} (single brace).
- Server restart required: Changing
kanban_dashboard_server.py doesn't auto-reload. Must run launchctl stop ai.{{AGENT_NAME_LOWER}}.kanban-dashboard && launchctl start ai.{{AGENT_NAME_LOWER}}.kanban-dashboard to apply changes.
- Python 3.14 compat: Server runs under Python 3.14.4 (from
.venv). Syntax is OK but test in the actual venv, not system python3.
- Modal JavaScript missing: The server generates HTML with
onclick="openModal('tid')" on each card, but openModal(taskId), switchTab(tab), and closeModal() must be defined in the inline <script> block. If these JS functions are missing (e.g. after editing the template f-strings), clicking cards silently does nothing. Verify their presence after any edit to the HTML template section. The modal also needs escapeHtml() for safe body rendering and event listeners for Escape/overlay-close.
- Card title truncation: Card titles are truncated to 60 chars on the board. Full title visible in modal header or card
title attribute (hover).
- Body parser regex:
_extract_summary() uses regex ## Topic\s*\n+(.*?)(?:\n\n|\n##) which assumes the body has a ## Topic heading followed by the summary paragraph. If the body format changes (e.g. no ## Topic), falls back to first non-empty non-heading line.
- Edit API idempotency:
POST /kanban/api/edit always updates body (even empty string) but only updates title if non-empty. To clear body, send body= (empty). The edit event is logged in task_events with kind='edited'.
- JS modal functions are in f-string:
openModal(), switchTab(), editBody(), saveBody(), cancelEdit(), escapeHtml() are all embedded in the Python f-string HTML template. Any syntax error in these JS functions breaks the entire modal. After editing, verify with curl -s http://macmini:8765/kanban | grep -c 'function openModal' (>0 means present).
- Card enrichment on page load:
_extract_summary() and _extract_source() run at page render time โ they add server-side compute per card. For boards with 50+ cards, consider caching or moving extraction to JS. Currently fine for <30 cards.
- Obsidian URL dependency:
obsidian://open requires (1) Obsidian.app installed on macOS, (2) browser-registered URL scheme handler. Links silently fail on non-macOS or without Obsidian. The _obsidian_url() function only generates URLs for paths under ~/.{{AGENT_NAME_LOWER}}/ โ other paths render as plain text.
- Draft path format sensitivity:
_extract_draft_path() relies on the exact section header ## Draft ํ์ผ ์์น in the body. If the content-pipeline changes this header (e.g. to ## Draft Location), draft links stop rendering silently. The regex also expects the path on the immediately following line.
- Modal draft link via JS regex: The Obsidian button in the modal Description tab is generated client-side by a regex against
task.body. This means if the body is edited via the dashboard (Edit button), the Obsidian link updates automatically on next modal open โ no page reload needed. But if the body is edited externally (DB direct, API), the modal shows stale links until reload.
Board UI Workflow (n8n)
Trigger
- Cron: Every 5 minutes (
*/5 * * * *)
Nodes
1. Cron Trigger (every 5min)
โ
2. SQLite Node โ Query {{AGENT_NAME_LOWER}}_tasks.db
SQL: |
SELECT id, title, status, assignee, created_at,
last_heartbeat_at, consecutive_failures
FROM tasks
WHERE board = 'default'
ORDER BY priority ASC NULLS LAST, created_at DESC
LIMIT 50
โ
3. Discord Bot Token (from .env: DISCORD_BOT_TOKEN)
โ
4. Discord Embed Builder (per status group)
โ
5. Edit Message โ post board to designated Discord channel
Board Embed Format
=== {{AGENT_NAME}} Kanban Board ===
Board: default | Updated: 2026-05-19 10:30 KST
[todo] 3 tasks
๐ก t_abc123 โ Implement kanban-dashboard skill
๐ก t_def456 โ Fix cycle detection bug
[ready] 2 tasks
โช t_ghi789 โ Deploy n8n workflow
[in_progress] 1 task
๐ต t_jkl012 โ kanban-orchestrator skill (worker: pid 12345)
[blocked] 1 task
๐ด t_mno345 โ gateway notifier (failures: 3)
[completed] 7 tasks (today: 2)
โ
t_pqr678 โ multi-board support
โ
t_stu901 โ activity logger integration
React to manage:
โ
= complete | ๐ = unblock | โ = block
Task Groups (by status)
| Status | Color | Emoji | Meaning |
|---|
| todo | ๐ก | YELLOW | Not yet ready |
| ready | โช | WHITE | Claimable |
| in_progress | ๐ต | BLUE | Worker active |
| blocked | ๐ด | RED | Waiting / failed |
| completed | โ
| GREEN | Done |
Reaction โ Action Workflow
When user reacts to the board message, n8n captures the reaction event:
1. Discord Reaction Event (add)
โ
2. Extract: message_id, emoji, user_id
โ
3. SQLite โ find task by id (from message content parsing)
โ
4. Switch on emoji:
- โ
โ kanban_complete(task_id, result="manual")
- ๐ โ kanban_unblock(task_id)
- โ โ kanban_block(task_id, reason="manual")
- ๐ โ kanban_claim(task_id, ttl_seconds=3600)
โ
5. Edit board message (refresh status)
Emoji Mapping
| Emoji | Action | Tool |
|---|
| โ
| Complete task | kanban_complete |
| ๐ | Unblock task | kanban_unblock |
| โ | Block task | kanban_block |
| ๐ | Claim task | kanban_claim |
Gateway Notifier (Phase 2)
Push task events to Discord subscribers.
SQLite Schema Addition
CREATE TABLE IF NOT EXISTS kanban_notify_subs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
platform TEXT NOT NULL,
chat_id TEXT NOT NULL,
thread_id TEXT,
subscriber TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE(task_id, platform, chat_id, subscriber)
);
CREATE INDEX IF NOT EXISTS idx_notify_task ON kanban_notify_subs(task_id);
Notifier Workflow
Task event fires (completed/blocked/crashed)
โ
1. Event Listener (from {{AGENT_NAME}}TaskStore.task_events table)
โ
2. Lookup subscribers for this task_id
โ
3. Per subscriber:
- Build Discord embed with event details
- Send via Discord webhook / bot
- Include: task title, status change, result summary
โ
4. Log notification in task_events
Notification Embed Format
๐ Task Completed
[content] multi-board support
Result: boards table + board-aware dispatch_once
Time: 2026-05-19 10:35 KST
Trigger: integration_workflow
n8n Workflow JSON (board poll)
{
"name": "{{AGENT_NAME}} Kanban Board",
"nodes": [
{
"name": "Cron Trigger",
"type": "n8n-nodes-base.cron",
"parameters": {
"rule": {"interval": [{"field": "minutes", "minutes": 5}]}
}
},
{
"name": "Query Tasks",
"type": "n8n-nodes-base.sql",
"parameters": {
"operation": "executeQuery",
"dataMode": "resolve",
"query": "SELECT id, title, status, assignee, created_at FROM tasks WHERE board = 'default' ORDER BY created_at DESC LIMIT 50"
}
},
{
"name": "Build Embed",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "// Group by status, build Discord embed JSON"
}
},
{
"name": "Post to Discord",
"type": "n8n-nodes-discord.api",
"parameters": {
"webhook": "{{$env.DISCORD_WEBHOOK_KANBAN}}"
}
}
]
}
Discord Channel Setup
Board message posted to: 1492883985473208522 (content-notify-channel)
Reaction events captured via Discord bot intents: GUILD_MESSAGES, MESSAGE_REACTION_ADD
Verification
- n8n workflow active and running
- Board embed posted to Discord channel
- Reaction โ task action confirmed
- Subscriber notifications delivered on task completion
Pitfalls (n8n/Discord)
- Poll frequency: 5min is default, too frequent (1min) may hit rate limits
- Message vs thread: Board message in channel, reactions on that message
- Emoji uniqueness: Multiple reactions from same user โ deduplicate by user_id + emoji
- Board refresh: After reaction action, edit the board message (not new message) to keep context
References
references/n8n-protocol.md โ n8n webhook protocol
references/kanban-modal-fix-20260615.md โ Modal openModal/switchTab implementation details
references/kanban-card-enrichment-20260615.md โ Card enrichment (body summary, category badge, source) + edit API + board cleanup protocol