- Gin HTTP server sa dashboard i API endpointima - JSON API: GET /api/tasks, GET /api/task/:id, POST /api/task/:id/move - HTML dashboard sa Kanban prikazom (5 kolona) - HTMX za interaktivnost (klik na task → detalj panel) - Embedded static fajlovi (htmx.min.js, sortable.min.js) - Config: dodat KAOS_PORT - 10 server testova, 77 ukupno — svi prolaze - Očišćeni duplikati taskova iz v0.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
5.1 KiB
Go
134 lines
5.1 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"strings"
|
|
|
|
"github.com/dal/kaos/internal/supervisor"
|
|
)
|
|
|
|
// statusIcons maps folder names to emoji icons.
|
|
var statusIcons = map[string]string{
|
|
"backlog": "📦",
|
|
"ready": "📋",
|
|
"active": "🔄",
|
|
"review": "👀",
|
|
"done": "✅",
|
|
}
|
|
|
|
// columnOrder defines the display order of columns.
|
|
var columnOrder = []string{"backlog", "ready", "active", "review", "done"}
|
|
|
|
// renderDashboard generates the full dashboard HTML page.
|
|
func renderDashboard(columns map[string][]supervisor.Task) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(`<!DOCTYPE html>
|
|
<html lang="sr">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>KAOS Dashboard</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #1a1a2e; color: #eee; }
|
|
.header { padding: 16px 24px; background: #16213e; display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #0f3460; }
|
|
.header h1 { font-size: 1.4em; }
|
|
.header .version { color: #888; font-size: 0.9em; }
|
|
.board { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; padding: 16px; min-height: calc(100vh - 60px); }
|
|
.column { background: #16213e; border-radius: 8px; padding: 12px; }
|
|
.column-header { font-weight: bold; padding: 8px; margin-bottom: 8px; border-bottom: 2px solid #0f3460; display: flex; justify-content: space-between; }
|
|
.column-count { background: #0f3460; border-radius: 12px; padding: 2px 8px; font-size: 0.85em; }
|
|
.task-card { background: #1a1a2e; border: 1px solid #333; border-radius: 6px; padding: 10px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.2s; }
|
|
.task-card:hover { border-color: #e94560; }
|
|
.task-id { font-weight: bold; color: #e94560; }
|
|
.task-title { margin-top: 4px; font-size: 0.9em; }
|
|
.task-meta { margin-top: 6px; font-size: 0.75em; color: #888; }
|
|
.task-deps { font-size: 0.75em; color: #666; margin-top: 4px; }
|
|
#task-detail { position: fixed; top: 0; right: 0; width: 400px; height: 100vh; background: #16213e; border-left: 2px solid #0f3460; padding: 20px; overflow-y: auto; display: none; z-index: 10; }
|
|
#task-detail.active { display: block; }
|
|
.detail-close { cursor: pointer; float: right; font-size: 1.2em; color: #888; }
|
|
.detail-close:hover { color: #e94560; }
|
|
.detail-content { white-space: pre-wrap; font-family: monospace; font-size: 0.85em; margin-top: 16px; line-height: 1.5; }
|
|
@media (max-width: 900px) { .board { grid-template-columns: repeat(3, 1fr); } }
|
|
@media (max-width: 600px) { .board { grid-template-columns: 1fr; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🔧 KAOS Dashboard</h1>
|
|
<span class="version">v0.2</span>
|
|
</div>
|
|
<div class="board" id="board">
|
|
`)
|
|
|
|
for _, col := range columnOrder {
|
|
tasks := columns[col]
|
|
icon := statusIcons[col]
|
|
b.WriteString(fmt.Sprintf(`<div class="column" id="col-%s" data-folder="%s">`, col, col))
|
|
b.WriteString(fmt.Sprintf(`<div class="column-header"><span>%s %s</span><span class="column-count">%d</span></div>`,
|
|
icon, strings.ToUpper(col), len(tasks)))
|
|
|
|
for _, t := range tasks {
|
|
b.WriteString(renderTaskCard(t))
|
|
}
|
|
b.WriteString("</div>\n")
|
|
}
|
|
|
|
b.WriteString(`</div>
|
|
<div id="task-detail"></div>
|
|
<script>
|
|
document.body.addEventListener('htmx:afterSwap', function(e) {
|
|
if (e.detail.target.id === 'task-detail') {
|
|
e.detail.target.classList.add('active');
|
|
}
|
|
});
|
|
function closeDetail() {
|
|
document.getElementById('task-detail').classList.remove('active');
|
|
document.getElementById('task-detail').innerHTML = '';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// renderTaskCard generates HTML for a single task card.
|
|
func renderTaskCard(t supervisor.Task) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(fmt.Sprintf(`<div class="task-card" data-id="%s" hx-get="/task/%s" hx-target="#task-detail" hx-swap="innerHTML">`,
|
|
t.ID, t.ID))
|
|
b.WriteString(fmt.Sprintf(`<div class="task-id">%s</div>`, html.EscapeString(t.ID)))
|
|
b.WriteString(fmt.Sprintf(`<div class="task-title">%s</div>`, html.EscapeString(t.Title)))
|
|
b.WriteString(fmt.Sprintf(`<div class="task-meta">%s · %s</div>`, html.EscapeString(t.Agent), html.EscapeString(t.Model)))
|
|
|
|
if len(t.DependsOn) > 0 {
|
|
b.WriteString(fmt.Sprintf(`<div class="task-deps">Zavisi od: %s</div>`, html.EscapeString(strings.Join(t.DependsOn, ", "))))
|
|
}
|
|
|
|
b.WriteString("</div>\n")
|
|
return b.String()
|
|
}
|
|
|
|
// renderTaskDetail generates HTML fragment for task detail panel.
|
|
func renderTaskDetail(t supervisor.Task, content string) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(`<span class="detail-close" onclick="closeDetail()">✕</span>`)
|
|
b.WriteString(fmt.Sprintf(`<h2>%s: %s</h2>`, html.EscapeString(t.ID), html.EscapeString(t.Title)))
|
|
b.WriteString(fmt.Sprintf(`<p>Agent: %s · Model: %s · Status: %s</p>`,
|
|
html.EscapeString(t.Agent), html.EscapeString(t.Model), html.EscapeString(t.Status)))
|
|
|
|
if len(t.DependsOn) > 0 {
|
|
b.WriteString(fmt.Sprintf(`<p>Zavisi od: %s</p>`, html.EscapeString(strings.Join(t.DependsOn, ", "))))
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf(`<div class="detail-content">%s</div>`, html.EscapeString(content)))
|
|
|
|
return b.String()
|
|
}
|