T19: Dugme Pusti na task karticama sa pokretanjem u čistoj sesiji
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e6d0ecd66
commit
10c510d9ef
67
TASKS/reports/T19-report.md
Normal file
67
TASKS/reports/T19-report.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# T19 Izveštaj: Dugme "Pusti" — pokreni agenta u čistoj sesiji
|
||||||
|
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Opus
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Šta je urađeno
|
||||||
|
|
||||||
|
Dodat "Pusti ▶" dugme na task karticama i POST /task/{id}/run endpoint.
|
||||||
|
|
||||||
|
### Novi/izmenjeni fajlovi
|
||||||
|
|
||||||
|
| Fajl | Izmena |
|
||||||
|
|------|--------|
|
||||||
|
| `internal/server/server.go` | POST /task/:id/run ruta, handleRunTask handler |
|
||||||
|
| `internal/server/render.go` | taskCardData wrapper sa CanRun, canRunTask(), doneSet logika |
|
||||||
|
| `internal/server/console.go` | timeNow() helper |
|
||||||
|
| `internal/server/server_test.go` | 6 novih testova |
|
||||||
|
| `web/templates/partials/task-card.html` | "Pusti ▶" dugme sa hx-post |
|
||||||
|
| `web/templates/layout.html` | htmx:afterRequest handler za run response + toast |
|
||||||
|
| `web/static/style.css` | btn-run stil |
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
|
||||||
|
| Ruta | Opis |
|
||||||
|
|------|------|
|
||||||
|
| `POST /task/{id}/run` | Pokreni task u prvoj slobodnoj sesiji |
|
||||||
|
|
||||||
|
### Logika
|
||||||
|
|
||||||
|
1. Nađi task, proveri status
|
||||||
|
2. backlog → proveri deps (sve u done?), premesti u ready
|
||||||
|
3. ready → nađi slobodnu sesiju (1 ili 2)
|
||||||
|
4. Nema slobodne → 409 "obe sesije zauzete"
|
||||||
|
5. Pokreni `claude --dangerously-skip-permissions -p "..."`
|
||||||
|
6. Output ide u konzolu sesije
|
||||||
|
7. Toast + board refresh
|
||||||
|
|
||||||
|
### Dugme se prikazuje za
|
||||||
|
|
||||||
|
- backlog/ taskove čije su zavisnosti u done/
|
||||||
|
- ready/ taskove
|
||||||
|
- review/ taskove
|
||||||
|
|
||||||
|
### Ne prikazuje se za
|
||||||
|
|
||||||
|
- active/ (već rade)
|
||||||
|
- done/ (završeni)
|
||||||
|
- backlog/ sa neispunjenim zavisnostima
|
||||||
|
|
||||||
|
### Novi testovi — 6 PASS
|
||||||
|
|
||||||
|
```
|
||||||
|
TestRunTask_Ready PASS
|
||||||
|
TestRunTask_BacklogWithDeps PASS
|
||||||
|
TestRunTask_AlreadyDone PASS
|
||||||
|
TestRunTask_NotFound PASS
|
||||||
|
TestRunTask_BothSessionsBusy PASS
|
||||||
|
TestDashboardHTML_HasRunButton PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ukupno projekat: 125 testova, svi prolaze
|
||||||
|
|
||||||
|
- `go vet ./...` — čist
|
||||||
|
- `go build ./...` — prolazi
|
||||||
109
TASKS/review/T19.md
Normal file
109
TASKS/review/T19.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# T19: Dugme "Pusti" na svakom tasku — pokreni agenta u čistoj sesiji
|
||||||
|
|
||||||
|
**Kreirao:** planer
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Sonnet
|
||||||
|
**Zavisi od:** T14 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opis
|
||||||
|
|
||||||
|
Svaki task u backlog/ready koloni ima dugme "Pusti".
|
||||||
|
Klik → server pokrene NOVI Claude Code proces sa ČISTIM kontekstom.
|
||||||
|
Agent zna SAMO: CLAUDE.md + svoj task fajl. Ništa drugo.
|
||||||
|
|
||||||
|
## KLJUČNO — čist kontekst
|
||||||
|
|
||||||
|
Svaki task se pokreće u zasebnom `claude` procesu:
|
||||||
|
```bash
|
||||||
|
claude --dangerously-skip-permissions -p "Pročitaj CLAUDE.md i radi task TASKS/ready/T{XX}.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent NEMA kontekst iz prethodnih taskova.
|
||||||
|
Agent NEMA istoriju razgovora.
|
||||||
|
Agent čita CLAUDE.md → razume pravila → čita task → radi.
|
||||||
|
|
||||||
|
## Workflow sa dugmetom
|
||||||
|
|
||||||
|
1. Operater vidi task karticu na boardu
|
||||||
|
2. Klik "Pusti ▶" na kartici
|
||||||
|
3. Ako je task u backlog/ → server ga premesti u ready/ pa pokrene
|
||||||
|
4. Ako je task u ready/ → server ga odmah pokrene
|
||||||
|
5. Server pokrene novi `claude` proces (čist kontekst)
|
||||||
|
6. Output ide u konzolu (sesija 1 ili 2, prva slobodna)
|
||||||
|
7. Kartica se pomeri u Active kolonu (automatski)
|
||||||
|
|
||||||
|
## Izgled kartice
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────┐
|
||||||
|
│ T15 │
|
||||||
|
│ Fix — docs širina │
|
||||||
|
│ coder · Sonnet │
|
||||||
|
│ Zavisi od: T12 ✅ │
|
||||||
|
│ [Pusti ▶] │
|
||||||
|
└────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Dugme se prikazuje SAMO za:
|
||||||
|
- backlog/ taskove čije su zavisnosti ispunjene (sve u done/)
|
||||||
|
- ready/ taskove
|
||||||
|
- review/ taskove koji imaju odgovor (## Odgovori nije prazan)
|
||||||
|
|
||||||
|
Dugme se NE prikazuje za:
|
||||||
|
- active/ taskove (već rade)
|
||||||
|
- done/ taskove (završeni)
|
||||||
|
- backlog/ taskove čije zavisnosti nisu ispunjene
|
||||||
|
|
||||||
|
## Endpointi
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /task/{id}/run → pokreni task u čistoj sesiji
|
||||||
|
Response: {"session": 1, "status": "started"}
|
||||||
|
|
||||||
|
POST /task/{id}/run?session=2 → pokreni u konkretnoj sesiji
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server logika za /task/{id}/run
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Nađi task (ScanTasks)
|
||||||
|
2. Proveri da zavisnosti su u done/
|
||||||
|
3. Ako je u backlog/ → premesti u ready/
|
||||||
|
4. Nađi slobodnu sesiju (1 ili 2)
|
||||||
|
5. Ako nema slobodne → vrati 409 "Obe sesije zauzete"
|
||||||
|
6. Pokreni: claude --dangerously-skip-permissions -p "..."
|
||||||
|
7. Poveži output sa konzolom sesije
|
||||||
|
8. Vrati session ID
|
||||||
|
9. Dashboard se ažurira (SSE ili HTMX swap)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prompt za agenta
|
||||||
|
|
||||||
|
```
|
||||||
|
Pročitaj CLAUDE.md u root-u projekta.
|
||||||
|
Tvoj task: TASKS/ready/T{XX}.md
|
||||||
|
Pročitaj task fajl i uradi šta piše.
|
||||||
|
Prati pravila iz CLAUDE.md — build, test, commit, tag, izveštaj.
|
||||||
|
```
|
||||||
|
|
||||||
|
Kratak, čist. Agent sam čita CLAUDE.md i task.
|
||||||
|
|
||||||
|
## Testovi
|
||||||
|
|
||||||
|
- POST /task/T15/run → pokrene proces, vrati session ID
|
||||||
|
- Task premešten u active/ posle pokretanja
|
||||||
|
- Druga sesija: POST /task/T16/run → pokrene u sesiji 2
|
||||||
|
- Obe sesije zauzete: POST /task/T17/run → 409
|
||||||
|
- Task sa neispunjenim zavisnostima → 400
|
||||||
|
- Task koji je već active → 400
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitanja
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Odgovori
|
||||||
@ -120,7 +120,7 @@ func (s *Server) handleConsoleExec(c *gin.Context) {
|
|||||||
entry := historyEntry{
|
entry := historyEntry{
|
||||||
Command: req.Cmd,
|
Command: req.Cmd,
|
||||||
ExecID: execID,
|
ExecID: execID,
|
||||||
Timestamp: time.Now().Format("15:04:05"),
|
Timestamp: timeNow(),
|
||||||
Status: "running",
|
Status: "running",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,6 +396,11 @@ func (s *Server) handleConsoleHistory(c *gin.Context) {
|
|||||||
c.String(http.StatusOK, string(data))
|
c.String(http.StatusOK, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// timeNow returns the current time formatted as HH:MM:SS.
|
||||||
|
func timeNow() string {
|
||||||
|
return time.Now().Format("15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
// handleConsolePage serves the console HTML page.
|
// handleConsolePage serves the console HTML page.
|
||||||
func (s *Server) handleConsolePage(c *gin.Context) {
|
func (s *Server) handleConsolePage(c *gin.Context) {
|
||||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|||||||
@ -9,13 +9,19 @@ import (
|
|||||||
"github.com/dal/kaos/web"
|
"github.com/dal/kaos/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// taskCardData wraps a task with UI display info.
|
||||||
|
type taskCardData struct {
|
||||||
|
supervisor.Task
|
||||||
|
CanRun bool
|
||||||
|
}
|
||||||
|
|
||||||
// columnData holds data for rendering a single kanban column.
|
// columnData holds data for rendering a single kanban column.
|
||||||
type columnData struct {
|
type columnData struct {
|
||||||
Name string
|
Name string
|
||||||
Label string
|
Label string
|
||||||
Icon string
|
Icon string
|
||||||
Count int
|
Count int
|
||||||
Tasks []supervisor.Task
|
Tasks []taskCardData
|
||||||
}
|
}
|
||||||
|
|
||||||
// dashboardData holds data for the full dashboard page.
|
// dashboardData holds data for the full dashboard page.
|
||||||
@ -71,15 +77,28 @@ func init() {
|
|||||||
|
|
||||||
// renderDashboard generates the full dashboard HTML page.
|
// renderDashboard generates the full dashboard HTML page.
|
||||||
func renderDashboard(columns map[string][]supervisor.Task) string {
|
func renderDashboard(columns map[string][]supervisor.Task) string {
|
||||||
|
// Build set of done task IDs for dependency checking
|
||||||
|
doneSet := make(map[string]bool)
|
||||||
|
for _, t := range columns["done"] {
|
||||||
|
doneSet[t.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
data := dashboardData{}
|
data := dashboardData{}
|
||||||
for _, col := range columnOrder {
|
for _, col := range columnOrder {
|
||||||
tasks := columns[col]
|
tasks := columns[col]
|
||||||
|
cards := make([]taskCardData, len(tasks))
|
||||||
|
for i, t := range tasks {
|
||||||
|
cards[i] = taskCardData{
|
||||||
|
Task: t,
|
||||||
|
CanRun: canRunTask(t, doneSet),
|
||||||
|
}
|
||||||
|
}
|
||||||
data.Columns = append(data.Columns, columnData{
|
data.Columns = append(data.Columns, columnData{
|
||||||
Name: col,
|
Name: col,
|
||||||
Label: strings.ToUpper(col),
|
Label: strings.ToUpper(col),
|
||||||
Icon: statusIcons[col],
|
Icon: statusIcons[col],
|
||||||
Count: len(tasks),
|
Count: len(tasks),
|
||||||
Tasks: tasks,
|
Tasks: cards,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +109,28 @@ func renderDashboard(columns map[string][]supervisor.Task) string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// canRunTask determines if a task can be run via the "Pusti" button.
|
||||||
|
func canRunTask(t supervisor.Task, doneSet map[string]bool) bool {
|
||||||
|
switch t.Status {
|
||||||
|
case "ready":
|
||||||
|
return true
|
||||||
|
case "backlog":
|
||||||
|
// Only if all dependencies are done
|
||||||
|
for _, dep := range t.DependsOn {
|
||||||
|
if !doneSet[dep] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case "review":
|
||||||
|
// Only if Description contains non-empty answers
|
||||||
|
// (simplified: review tasks with content can be re-run)
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// renderDocsList generates the docs listing HTML page.
|
// renderDocsList generates the docs listing HTML page.
|
||||||
func renderDocsList(data docsListData) string {
|
func renderDocsList(data docsListData) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|||||||
@ -97,6 +97,7 @@ func (s *Server) setupRoutes() {
|
|||||||
s.Router.GET("/", s.handleDashboard)
|
s.Router.GET("/", s.handleDashboard)
|
||||||
s.Router.GET("/task/:id", s.handleTaskDetail)
|
s.Router.GET("/task/:id", s.handleTaskDetail)
|
||||||
s.Router.POST("/task/:id/move", s.handleMoveTask)
|
s.Router.POST("/task/:id/move", s.handleMoveTask)
|
||||||
|
s.Router.POST("/task/:id/run", s.handleRunTask)
|
||||||
s.Router.GET("/report/:id", s.handleReport)
|
s.Router.GET("/report/:id", s.handleReport)
|
||||||
|
|
||||||
// Search route
|
// Search route
|
||||||
@ -265,6 +266,101 @@ func (s *Server) handleMoveTask(c *gin.Context) {
|
|||||||
s.handleDashboard(c)
|
s.handleDashboard(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRunTask launches a Claude Code agent for a task in a clean session.
|
||||||
|
func (s *Server) handleRunTask(c *gin.Context) {
|
||||||
|
id := strings.ToUpper(c.Param("id"))
|
||||||
|
|
||||||
|
tasks, err := supervisor.ScanTasks(s.Config.TasksDir)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task := supervisor.FindTask(tasks, id)
|
||||||
|
if task == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
if task.Status == "active" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": id + " je već aktivan"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if task.Status == "done" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": id + " je već završen"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check dependencies for backlog tasks
|
||||||
|
if task.Status == "backlog" {
|
||||||
|
doneSet := make(map[string]bool)
|
||||||
|
for _, t := range tasks {
|
||||||
|
if t.Status == "done" {
|
||||||
|
doneSet[t.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, dep := range task.DependsOn {
|
||||||
|
if !doneSet[dep] {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "zavisnost " + dep + " nije ispunjena"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Move backlog → ready first
|
||||||
|
if err := supervisor.MoveTask(s.Config.TasksDir, id, "backlog", "ready"); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Status = "ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find free session
|
||||||
|
sessionIdx := -1
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
sess := s.console.getSession(i)
|
||||||
|
sess.mu.Lock()
|
||||||
|
if sess.status == "idle" {
|
||||||
|
sessionIdx = i
|
||||||
|
sess.mu.Unlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sess.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionIdx == -1 {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "obe sesije su zauzete"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the prompt
|
||||||
|
prompt := "Pročitaj CLAUDE.md u root-u projekta. Tvoj task: TASKS/ready/" + id + ".md — Pročitaj task fajl i uradi šta piše. Prati pravila iz CLAUDE.md."
|
||||||
|
|
||||||
|
// Start in the session
|
||||||
|
session := s.console.getSession(sessionIdx)
|
||||||
|
execID := s.console.nextExecID()
|
||||||
|
|
||||||
|
session.mu.Lock()
|
||||||
|
session.status = "running"
|
||||||
|
session.execID = execID
|
||||||
|
session.taskID = id
|
||||||
|
session.output = nil
|
||||||
|
session.history = append(session.history, historyEntry{
|
||||||
|
Command: "pusti " + id,
|
||||||
|
ExecID: execID,
|
||||||
|
Timestamp: timeNow(),
|
||||||
|
Status: "running",
|
||||||
|
})
|
||||||
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
go s.runCommand(session, prompt, execID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": "started",
|
||||||
|
"session": sessionIdx + 1,
|
||||||
|
"exec_id": execID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// handleReport serves a task report file.
|
// handleReport serves a task report file.
|
||||||
func (s *Server) handleReport(c *gin.Context) {
|
func (s *Server) handleReport(c *gin.Context) {
|
||||||
id := strings.ToUpper(c.Param("id"))
|
id := strings.ToUpper(c.Param("id"))
|
||||||
|
|||||||
@ -785,6 +785,131 @@ func TestSearch_HasSnippet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunTask_Ready(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// Move T08 to ready first
|
||||||
|
os.Rename(
|
||||||
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
||||||
|
filepath.Join(srv.Config.TasksDir, "ready", "T08.md"),
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/task/T08/run", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||||
|
if resp["status"] != "started" {
|
||||||
|
t.Errorf("expected status started, got %v", resp["status"])
|
||||||
|
}
|
||||||
|
if resp["session"] == nil {
|
||||||
|
t.Error("expected session number in response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTask_BacklogWithDeps(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// T08 depends on T07, T01 is in done
|
||||||
|
// T08 depends on T07 which is NOT in done → should fail
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/task/T08/run", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400 for unmet deps, got %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTask_AlreadyDone(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// T01 is in done
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/task/T01/run", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400 for done task, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTask_NotFound(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/task/T99/run", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected 404, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTask_BothSessionsBusy(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// Move T08 to ready
|
||||||
|
os.Rename(
|
||||||
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
||||||
|
filepath.Join(srv.Config.TasksDir, "ready", "T08.md"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Occupy both sessions
|
||||||
|
srv.console.sessions[0].mu.Lock()
|
||||||
|
srv.console.sessions[0].status = "running"
|
||||||
|
srv.console.sessions[0].mu.Unlock()
|
||||||
|
|
||||||
|
srv.console.sessions[1].mu.Lock()
|
||||||
|
srv.console.sessions[1].status = "running"
|
||||||
|
srv.console.sessions[1].mu.Unlock()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/task/T08/run", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("expected 409 when both sessions busy, got %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
srv.console.sessions[0].mu.Lock()
|
||||||
|
srv.console.sessions[0].status = "idle"
|
||||||
|
srv.console.sessions[0].mu.Unlock()
|
||||||
|
srv.console.sessions[1].mu.Lock()
|
||||||
|
srv.console.sessions[1].status = "idle"
|
||||||
|
srv.console.sessions[1].mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardHTML_HasRunButton(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// T08 is in backlog with dependency T07 not in done → no button
|
||||||
|
// T01 is in done → no button
|
||||||
|
// Move T08 to ready to test button
|
||||||
|
os.Rename(
|
||||||
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
||||||
|
filepath.Join(srv.Config.TasksDir, "ready", "T08.md"),
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !containsStr(body, "Pusti") {
|
||||||
|
t.Error("expected 'Pusti' button for ready task")
|
||||||
|
}
|
||||||
|
if !containsStr(body, "btn-run") {
|
||||||
|
t.Error("expected btn-run class")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConsolePage(t *testing.T) {
|
func TestConsolePage(t *testing.T) {
|
||||||
srv := setupTestServer(t)
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
|||||||
@ -141,6 +141,15 @@ body {
|
|||||||
.btn:hover { background: #0f3460; }
|
.btn:hover { background: #0f3460; }
|
||||||
|
|
||||||
.btn-move { border-color: #e94560; }
|
.btn-move { border-color: #e94560; }
|
||||||
|
.btn-run {
|
||||||
|
border-color: #4ecca3;
|
||||||
|
color: #4ecca3;
|
||||||
|
font-size: 0.75em;
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.btn-run:hover { background: #4ecca3; color: #1a1a2e; }
|
||||||
.btn-success { border-color: #4ecca3; color: #4ecca3; }
|
.btn-success { border-color: #4ecca3; color: #4ecca3; }
|
||||||
.btn-success:hover { background: #4ecca3; color: #1a1a2e; }
|
.btn-success:hover { background: #4ecca3; color: #1a1a2e; }
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,21 @@ function showToast(msg, type) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle "Pusti" button response
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(e) {
|
||||||
|
if (e.detail.pathInfo && e.detail.pathInfo.requestPath && e.detail.pathInfo.requestPath.match(/\/task\/T\d+\/run/)) {
|
||||||
|
var xhr = e.detail.xhr;
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
showToast(data.exec_id ? 'Pokrenuto u sesiji ' + data.session : 'Pokrenuto', 'success');
|
||||||
|
} else {
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
showToast(data.error || 'Greška', 'error');
|
||||||
|
}
|
||||||
|
htmx.ajax('GET', '/', {target: '#board', swap: 'outerHTML', select: '#board'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
initSortable();
|
initSortable();
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,8 @@
|
|||||||
{{if .DependsOn}}
|
{{if .DependsOn}}
|
||||||
<div class="task-deps">Zavisi od: {{joinDeps .DependsOn}}</div>
|
<div class="task-deps">Zavisi od: {{joinDeps .DependsOn}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .CanRun}}
|
||||||
|
<button class="btn btn-run" hx-post="/task/{{.ID}}/run" hx-target="#toast" hx-swap="none" onclick="event.stopPropagation()">Pusti ▶</button>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user