diff --git a/index.html b/index.html index 78a2d80..3b100cc 100644 --- a/index.html +++ b/index.html @@ -402,7 +402,9 @@
🐙 GITEA KANBAN & PRS
- 2 open PRs | + 0 PRs | + 0 rev | + 0 wip | CONNECTED
@@ -692,11 +694,17 @@ } // 3. Update Gitea HUD & Kanban - document.getElementById('giteaPrs').innerText = data.gitea.open_prs_count; + document.getElementById('giteaPrs').innerText = data.gitea.open_prs_count || 0; const gStatus = document.getElementById('giteaStatus'); gStatus.innerText = data.gitea.connected ? 'CONNECTED' : 'DISCONNECTED'; gStatus.className = data.gitea.connected ? 'badge-ok' : 'badge-busy'; - if (data.gitea.kanban_columns) kanbanState = data.gitea.kanban_columns; + if (data.gitea.kanban_columns) { + kanbanState = data.gitea.kanban_columns; + const revElem = document.getElementById('giteaReview'); + if (revElem) revElem.innerText = kanbanState.review || 0; + const wipElem = document.getElementById('giteaWip'); + if (wipElem) wipElem.innerText = kanbanState.in_progress || 0; + } // 4. Update Agents if (data.agents) { @@ -984,40 +992,63 @@ ctx.fillText(f.label, f.x + 12, f.y + h + 24); } else if (f.type === 'WHITEBOARD') { - // Gitea Kanban Board on the Wall! - const w = 110, h = 50; + // Realtime Gitea Kanban Board on the Wall! + const w = 120, h = 54; ctx.fillStyle = "#ffffff"; ctx.fillRect(f.x, f.y, w, h); - ctx.strokeStyle = "#94a3b8"; + ctx.strokeStyle = "#64748b"; ctx.lineWidth = 2; ctx.strokeRect(f.x, f.y, w, h); - // Kanban Columns: Ready | WIP | Review - ctx.strokeStyle = "#cbd5e1"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(f.x + 36, f.y); ctx.lineTo(f.x + 36, f.y + h); - ctx.moveTo(f.x + 72, f.y); ctx.lineTo(f.x + 72, f.y + h); - ctx.stroke(); - + // Header Bar + ctx.fillStyle = "#1e293b"; + ctx.fillRect(f.x, f.y, w, 12); ctx.font = "8px 'Press Start 2P'"; - ctx.fillStyle = "#0f172a"; - ctx.fillText("RDY", f.x + 6, f.y + 10); - ctx.fillText("WIP", f.x + 42, f.y + 10); - ctx.fillText("REV", f.x + 78, f.y + 10); + ctx.fillStyle = "#facc15"; + ctx.fillText("KANBAN BOARD", f.x + 8, f.y + 9); - // Colored Sticky notes based on actual kanban counts - for (let i = 0; i < Math.min(kanbanState.ready, 3); i++) { - ctx.fillStyle = "#facc15"; // Yellow ready sticky - ctx.fillRect(f.x + 8, f.y + 14 + i * 11, 20, 8); - } - for (let i = 0; i < Math.min(kanbanState.in_progress, 2); i++) { - ctx.fillStyle = "#38bdf8"; // Blue wip sticky - ctx.fillRect(f.x + 44, f.y + 14 + i * 11, 20, 8); - } - for (let i = 0; i < Math.min(kanbanState.review, 3); i++) { - ctx.fillStyle = "#f43f5e"; // Pink review sticky - ctx.fillRect(f.x + 80, f.y + 14 + i * 11, 20, 8); + // 4 Columns: Plan | WIP | Review | Done + const colW = w / 4; + const colColors = ["#94a3b8", "#38bdf8", "#ec4899", "#22c55e"]; + const colHeaders = ["PLN", "WIP", "REV", "OK"]; + const colCounts = [ + kanbanState.planned || 0, + kanbanState.in_progress || 0, + kanbanState.review || 0, + kanbanState.done || 0 + ]; + + for (let c = 0; c < 4; c++) { + const cx = f.x + c * colW; + if (c > 0) { + ctx.strokeStyle = "#e2e8f0"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(cx, f.y + 12); ctx.lineTo(cx, f.y + h); + ctx.stroke(); + } + + // Column Header Label + ctx.font = "7px 'Press Start 2P'"; + ctx.fillStyle = "#475569"; + ctx.fillText(colHeaders[c], cx + 4, f.y + 21); + + // Draw Mini Sticky Notes + const count = colCounts[c]; + const stickiesToShow = Math.min(count, 3); + for (let s = 0; s < stickiesToShow; s++) { + ctx.fillStyle = colColors[c]; + ctx.fillRect(cx + 4, f.y + 24 + s * 9, colW - 8, 7); + ctx.strokeStyle = "rgba(0,0,0,0.3)"; + ctx.strokeRect(cx + 4, f.y + 24 + s * 9, colW - 8, 7); + } + + // Counter Badge if count > 0 + if (count > 0) { + ctx.font = "10px 'VT323'"; + ctx.fillStyle = "#0f172a"; + ctx.fillText(count.toString(), cx + colW - 10, f.y + 21); + } } } else if (f.type === 'BULLMQ_STATION') { diff --git a/server.py b/server.py index cace31b..ec3ccf5 100644 --- a/server.py +++ b/server.py @@ -149,11 +149,12 @@ class RealtimeRPGHandler(http.server.SimpleHTTPRequestHandler): status["gitea"]["active_ci_jobs"] = len(ci_containers) status["gitea"]["ci_status"] = "BUILDING" if len(ci_containers) > 0 else "PASSING" - req = urllib.request.Request( + # Fetch Open Pull Requests + req_prs = urllib.request.Request( f"{GITEA_BASE_URL}/repos/openclaw/{target_project}/pulls?state=open", headers={'Authorization': f'token {GITEA_TOKEN}'} ) - with urllib.request.urlopen(req, timeout=2) as resp: + with urllib.request.urlopen(req_prs, timeout=2) as resp: prs = json.loads(resp.read().decode()) status["gitea"]["open_prs_count"] = len(prs) for p in prs: @@ -163,10 +164,51 @@ class RealtimeRPGHandler(http.server.SimpleHTTPRequestHandler): "user": p.get("user", {}).get("username"), "created_at": p.get("created_at") }) - status["gitea"]["kanban_columns"]["review"] = len(prs) - status["gitea"]["kanban_columns"]["ready"] = max(1, len(prs)) - status["gitea"]["kanban_columns"]["planned"] = 3 - status["gitea"]["kanban_columns"]["done"] = 12 + + # Fetch All Issues for Real Kanban Board Sync + req_issues = urllib.request.Request( + f"{GITEA_BASE_URL}/repos/openclaw/{target_project}/issues?state=all&limit=50", + headers={'Authorization': f'token {GITEA_TOKEN}'} + ) + with urllib.request.urlopen(req_issues, timeout=2) as resp: + issues = json.loads(resp.read().decode()) + kanban = {"planned": 0, "ready": 0, "in_progress": 0, "review": 0, "done": 0} + kanban_items = {"planned": [], "ready": [], "in_progress": [], "review": [], "done": []} + + for iss in issues: + labels = [l.get("name", "").lower() for l in iss.get("labels", [])] + is_closed = iss.get("state") == "closed" + is_pr = bool(iss.get("pull_request")) + + item_meta = { + "number": iss.get("number"), + "title": iss.get("title"), + "is_pr": is_pr, + "state": iss.get("state") + } + + if is_closed: + kanban["done"] += 1 + if len(kanban_items["done"]) < 5: kanban_items["done"].append(item_meta) + elif any("review" in l for l in labels) or is_pr: + kanban["review"] += 1 + if len(kanban_items["review"]) < 5: kanban_items["review"].append(item_meta) + elif any("in-progress" in l or "in_progress" in l for l in labels) or ("coder-tasks" in latest_active_tasks): + kanban["in_progress"] += 1 + if len(kanban_items["in_progress"]) < 5: kanban_items["in_progress"].append(item_meta) + elif any("ready" in l or "board/active" in l for l in labels): + kanban["ready"] += 1 + if len(kanban_items["ready"]) < 5: kanban_items["ready"].append(item_meta) + else: + kanban["planned"] += 1 + if len(kanban_items["planned"]) < 5: kanban_items["planned"].append(item_meta) + + # Ensure active BullMQ coder task reflects in Kanban WIP if not already + if status["bullmq"]["total_active"] > 0 and kanban["in_progress"] == 0: + kanban["in_progress"] = status["bullmq"]["total_active"] + + status["gitea"]["kanban_columns"] = kanban + status["gitea"]["kanban_items"] = kanban_items except Exception as e: status["gitea"]["connected"] = False status["gitea"]["error"] = str(e)