T08: HTTP server + API za taskove
- 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>
This commit is contained in:
parent
bd62320642
commit
04ef8e75ef
23
CLAUDE.md
23
CLAUDE.md
@ -62,16 +62,17 @@ TASKS/
|
||||
└── reports/ ← izveštaji izvršenih taskova
|
||||
```
|
||||
|
||||
### Ko šta radi
|
||||
### Ko šta sme da premesti
|
||||
|
||||
| Folder | Ko piše | Ko čita | Ko premešta |
|
||||
|--------|---------|---------|-------------|
|
||||
| backlog/ | planer | operater | operater → ready/ |
|
||||
| ready/ | — | agent | agent → active/ |
|
||||
| active/ | agent | agent | agent → review/ |
|
||||
| review/ | planer (odgovori) | operater, agent | operater → done/ ili agent → active/ |
|
||||
| done/ | — | svi | nikad |
|
||||
| reports/ | agent | svi | nikad |
|
||||
| Iz → U | Operater (dashboard) | Agent (CLI) |
|
||||
|---------|---------------------|-------------|
|
||||
| backlog → ready | ✅ | ❌ |
|
||||
| ready → backlog | ✅ | ❌ |
|
||||
| ready → active | ❌ | ✅ |
|
||||
| active → review | ❌ | ✅ |
|
||||
| review → done | ✅ | ❌ |
|
||||
| review → ready | ✅ | ❌ |
|
||||
| done → bilo gde | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
@ -116,8 +117,8 @@ TASKS/
|
||||
|-------|--------|-------|---------|
|
||||
| Triage | agents/triage/ | Haiku | 0.1.0 |
|
||||
| Task Manager | agents/task-manager/ | Sonnet/Haiku | 0.1.0 |
|
||||
| Coder | agents/coder/ | Sonnet/Opus | 0.1.0 |
|
||||
| Frontend | agents/frontend/ | Sonnet | 0.1.0 |
|
||||
| Coder | agents/coder/ | Sonnet/Opus | 0.2.0 |
|
||||
| Frontend | agents/frontend/ | Sonnet | 0.2.0 |
|
||||
| Checker | agents/checker/ | Haiku/Opus | 0.1.0 |
|
||||
| Reporter | agents/reporter/ | Haiku | 0.1.0 |
|
||||
| Docs | agents/docs/ | Haiku | 0.1.0 |
|
||||
|
||||
@ -161,7 +161,7 @@ Deploy ili dorada
|
||||
| Timeout | Ručno podešavanje, operater odlučuje | Feb 2026 |
|
||||
| Troškovi | Praćenje po tasku (tokeni, cena, vreme) | Feb 2026 |
|
||||
| Backend | Go | Feb 2026 |
|
||||
| Frontend | React + TypeScript + Vite + Tailwind + shadcn/ui | Feb 2026 |
|
||||
| Frontend | Go templates + HTMX + Sortable.js (nula npm) | Feb 2026 |
|
||||
| Baza | PostgreSQL (v0.2+) | Feb 2026 |
|
||||
| HTTP framework | Gin (v0.2+) | Feb 2026 |
|
||||
| Engine | `pkg/engine/` javni paket, nula HTTP (v0.2+) | Feb 2026 |
|
||||
|
||||
@ -23,24 +23,33 @@
|
||||
|
||||
| Folder | Sadržaj | Taskovi |
|
||||
|--------|---------|---------|
|
||||
| backlog/ | Čeka odobrenje | T01 |
|
||||
| backlog/ | Čeka odobrenje | T08, T09, T10 |
|
||||
| ready/ | Odobren za rad | — |
|
||||
| active/ | U izradi | — |
|
||||
| review/ | Čeka pregled/odgovor | — |
|
||||
| done/ | Završeno | — |
|
||||
| done/ | Završeno | T01, T02, T03, T04, T05, T06, T07 |
|
||||
|
||||
---
|
||||
|
||||
## v0.1 Taskovi
|
||||
## v0.1 Taskovi — ZAVRŠENO ✅
|
||||
|
||||
| Task | Naslov | Tag | Commit | Testova |
|
||||
|------|--------|-----|--------|---------|
|
||||
| T01 | Inicijalizacija Go projekta | v0.1.1 | f001c53 | 6 |
|
||||
| T02 | Task loader (parsiranje MD) | v0.1.2 | 79bcd52 | 17 |
|
||||
| T03 | Runner (pokretanje Claude Code) | v0.1.4 | 9d2c249 | 7 |
|
||||
| T04 | Checker (build + test + vet) | v0.1.3 | 5d869f5 | 10 |
|
||||
| T05 | Reporter (pisanje izveštaja) | v0.1.5 | 028872b | 10 |
|
||||
| T06 | CLI (komandni interfejs) | v0.1.6 | 38e1e10 | 9 |
|
||||
| T07 | Integracija (end-to-end) | v0.1.7 | b2ece98 | 8 |
|
||||
| **Ukupno** | | | | **67** |
|
||||
|
||||
---
|
||||
|
||||
## Sledeće — v0.2 Dashboard
|
||||
|
||||
| Task | Naslov | Folder | Zavisi od |
|
||||
|------|--------|--------|-----------|
|
||||
| T01 | Inicijalizacija Go projekta | backlog | — |
|
||||
| T02 | Task loader (parsiranje MD) | — | T01 |
|
||||
| T03 | Runner (pokretanje Claude Code) | — | T02 |
|
||||
| T04 | Checker (build + test + vet) | — | T01 |
|
||||
| T05 | Reporter (pisanje izveštaja) | — | T03, T04 |
|
||||
| T06 | CLI (komandni interfejs) | — | T02-T05 |
|
||||
| T07 | Integracija (end-to-end) | — | T06 |
|
||||
|
||||
T02-T07 će biti napisani u backlog/ kad T01 bude done.
|
||||
| T08 | HTTP server + API | backlog | T07 ✅ |
|
||||
| T09 | Dashboard kanban board | backlog | T08 |
|
||||
| T10 | Drag & Drop | backlog | T09 |
|
||||
|
||||
106
TASKS/backlog/T09.md
Normal file
106
TASKS/backlog/T09.md
Normal file
@ -0,0 +1,106 @@
|
||||
# T09: Dashboard — Kanban board sa taskovima
|
||||
|
||||
**Kreirao:** planer
|
||||
**Datum:** 2026-02-20
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** T08
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
HTML dashboard sa Kanban prikazom — kolone po stanju
|
||||
(backlog, ready, active, review, done). HTMX za interaktivnost.
|
||||
|
||||
## Fajlovi za kreiranje
|
||||
|
||||
```
|
||||
code/web/
|
||||
├── templates/
|
||||
│ ├── layout.html ← osnovna struktura (head, body, footer)
|
||||
│ ├── dashboard.html ← kanban board
|
||||
│ ├── partials/
|
||||
│ │ ├── column.html ← jedna kolona (HTMX fragment)
|
||||
│ │ ├── task-card.html ← kartica taska
|
||||
│ │ └── task-detail.html ← detalj taska (klik → prikaz sadržaja)
|
||||
└── static/
|
||||
└── style.css ← stilovi za dashboard
|
||||
```
|
||||
|
||||
## Izgled
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔧 KAOS Dashboard v0.1.7 │
|
||||
├──────────┬──────────┬──────────┬──────────┬─────────────┤
|
||||
│ BACKLOG │ READY │ ACTIVE │ REVIEW │ DONE │
|
||||
│ 2 │ 1 │ - │ - │ 7 │
|
||||
├──────────┼──────────┼──────────┼──────────┼─────────────┤
|
||||
│┌────────┐│┌────────┐│ │ │┌───────────┐│
|
||||
││ T08 │││ T10 ││ │ ││ T01 ✅ ││
|
||||
││ Server │││ Drag ││ │ ││ Go init ││
|
||||
││ Sonnet │││ & Drop ││ │ ││ v0.1.1 ││
|
||||
│└────────┘│└────────┘│ │ │└───────────┘│
|
||||
│┌────────┐│ │ │ │┌───────────┐│
|
||||
││ T09 ││ │ │ ││ T02 ✅ ││
|
||||
││ Dashb. ││ │ │ ││ Loader ││
|
||||
│└────────┘│ │ │ ││ v0.1.2 ││
|
||||
│ │ │ │ │└───────────┘│
|
||||
│ │ │ │ │ ... │
|
||||
└──────────┴──────────┴──────────┴──────────┴─────────────┘
|
||||
```
|
||||
|
||||
## Kartica taska
|
||||
|
||||
Prikazuje:
|
||||
- ID (T01, T02...)
|
||||
- Naslov
|
||||
- Agent + Model
|
||||
- Tag verzije (ako je done)
|
||||
- Zavisnosti
|
||||
|
||||
Klik na karticu → HTMX učita detalj:
|
||||
```html
|
||||
<div class="task-card" hx-get="/task/T01" hx-target="#task-detail">
|
||||
```
|
||||
|
||||
## Task detalj panel
|
||||
|
||||
Desna strana ili modal — prikazuje ceo sadržaj task fajla:
|
||||
- Markdown renderovan kao HTML
|
||||
- Dugme za premestanje u sledeći folder
|
||||
- Link do izveštaja (ako postoji)
|
||||
|
||||
## HTMX interakcije
|
||||
|
||||
- Klik na task → `hx-get="/task/{id}"` → prikaz detalja
|
||||
- Dugme "Premesti" → `hx-post="/task/{id}/move?to=ready"` → ažurira kolonu
|
||||
- Auto-refresh → `hx-trigger="every 5s"` na active koloni
|
||||
|
||||
## Pravila
|
||||
|
||||
- Go `html/template` za renderovanje
|
||||
- Mobilno responsive
|
||||
- Poruke na srpskom
|
||||
- Nema JS osim htmx.min.js
|
||||
- CSS grid za kolone
|
||||
|
||||
## Testovi
|
||||
|
||||
- GET / → vraća HTML sa svim kolonama
|
||||
- Proveri da su taskovi u pravim kolonama
|
||||
- HTMX fragment: GET /task/T01 → vraća HTML fragment
|
||||
|
||||
## Očekivani izlaz
|
||||
|
||||
Otvori http://localhost:8080 → vidi kanban board sa taskovima.
|
||||
Klikni na task → vidi detalj.
|
||||
|
||||
---
|
||||
|
||||
## Pitanja
|
||||
|
||||
---
|
||||
|
||||
## Odgovori
|
||||
90
TASKS/backlog/T10.md
Normal file
90
TASKS/backlog/T10.md
Normal file
@ -0,0 +1,90 @@
|
||||
# T10: Drag & Drop — premesti task prevlačenjem
|
||||
|
||||
**Kreirao:** planer
|
||||
**Datum:** 2026-02-20
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** T09
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
Dodaj Sortable.js na kanban board — prevuci task iz jedne kolone
|
||||
u drugu. Na drop, HTMX pošalje POST i Go premesti fajl.
|
||||
|
||||
## Fajlovi za izmenu
|
||||
|
||||
```
|
||||
code/web/
|
||||
├── templates/
|
||||
│ ├── dashboard.html ← dodaj Sortable inicijalizaciju
|
||||
│ └── partials/
|
||||
│ └── column.html ← dodaj sortable atribute
|
||||
└── static/
|
||||
└── style.css ← drag stilovi (ghost, placeholder)
|
||||
```
|
||||
|
||||
## Kako radi
|
||||
|
||||
```html
|
||||
<div class="column" id="col-ready" data-folder="ready">
|
||||
<div class="task-card" data-id="T08">...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.column').forEach(col => {
|
||||
new Sortable(col, {
|
||||
group: 'tasks',
|
||||
animation: 150,
|
||||
onEnd: function(evt) {
|
||||
const taskId = evt.item.dataset.id;
|
||||
const toFolder = evt.to.dataset.folder;
|
||||
htmx.ajax('POST', `/task/${taskId}/move?to=${toFolder}`, {
|
||||
target: '#board',
|
||||
swap: 'outerHTML'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Pravila premestanja
|
||||
|
||||
Dozvoljena kretanja:
|
||||
- backlog → ready (operater odobri)
|
||||
- ready → backlog (operater povuče nazad)
|
||||
- review → done (operater odobri)
|
||||
- review → ready (operater vrati na doradu)
|
||||
|
||||
Zabranjena kretanja (agent radi ovo, ne operater):
|
||||
- ready → active (samo agent)
|
||||
- active → review (samo agent)
|
||||
|
||||
Server validira i odbije nedozvoljene poteze sa porukom.
|
||||
|
||||
## Vizuelni feedback
|
||||
|
||||
- Drag: kartica postaje poluprozirna
|
||||
- Drop zona: highlight kad se kartica prevlači iznad
|
||||
- Uspešan drop: zeleni flash
|
||||
- Neuspešan drop: crveni flash + kartica se vrati
|
||||
|
||||
## Testovi
|
||||
|
||||
- Premesti T08 iz backlog u ready → fajl premešten, board ažuriran
|
||||
- Pokušaj premesti u active → server odbije, kartica se vrati
|
||||
- Drag & drop ne kvari postojeći klik za detalj
|
||||
|
||||
## Očekivani izlaz
|
||||
|
||||
Prevuci task iz kolone u kolonu. Server premesti fajl. Board se ažurira.
|
||||
|
||||
---
|
||||
|
||||
## Pitanja
|
||||
|
||||
---
|
||||
|
||||
## Odgovori
|
||||
104
TASKS/ready/T07.md
Normal file
104
TASKS/ready/T07.md
Normal file
@ -0,0 +1,104 @@
|
||||
# T07: Integracija — sve zajedno
|
||||
|
||||
**Kreirao:** planer
|
||||
**Datum:** 2026-02-20
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** T06
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
End-to-end tok: CLI pozove run → učita task → pokrene agenta →
|
||||
verifikuje → napiše izveštaj → premesti task. Sve komponente
|
||||
povezane u jedan flow.
|
||||
|
||||
## Fajlovi za izmenu
|
||||
|
||||
```
|
||||
code/internal/supervisor/
|
||||
├── supervisor.go ← Supervisor struct, Run() metoda
|
||||
└── supervisor_test.go ← end-to-end testovi
|
||||
```
|
||||
|
||||
## Supervisor struct
|
||||
|
||||
```go
|
||||
type Supervisor struct {
|
||||
Config *config.Config
|
||||
TasksDir string
|
||||
CodeDir string
|
||||
ReportsDir string
|
||||
}
|
||||
|
||||
func NewSupervisor(cfg *config.Config) *Supervisor
|
||||
func (s *Supervisor) Run(taskID string) error
|
||||
func (s *Supervisor) RunNext() error
|
||||
```
|
||||
|
||||
## Run() tok
|
||||
|
||||
```go
|
||||
func (s *Supervisor) Run(taskID string) error {
|
||||
// 1. ScanTasks(s.TasksDir)
|
||||
// 2. FindTask(taskID) — proveri da je u ready/
|
||||
// 3. MoveTask → active/
|
||||
// 4. RunTask(task, s.CodeDir, s.Config.Timeout)
|
||||
// 5. Verify(s.CodeDir)
|
||||
// 6. WriteReport(task, runResult, verifyResult, s.ReportsDir)
|
||||
// 7. Ako AllPassed → MoveTask → review/
|
||||
// 8. Ako !AllPassed → MoveTask → review/ (sa statusom failed u izveštaju)
|
||||
// 9. Prikaži rezime
|
||||
}
|
||||
```
|
||||
|
||||
## RunNext() tok
|
||||
|
||||
```go
|
||||
func (s *Supervisor) RunNext() error {
|
||||
// 1. ScanTasks
|
||||
// 2. NextTask — prvi iz ready/ sa ispunjenim zavisnostima
|
||||
// 3. Ako nema → vrati poruku "nema taskova"
|
||||
// 4. Run(task.ID)
|
||||
}
|
||||
```
|
||||
|
||||
## Integracija sa CLI
|
||||
|
||||
main.go poziva:
|
||||
- `run T01` → supervisor.Run("T01")
|
||||
- `run` (bez ID) → supervisor.RunNext()
|
||||
- `status` → ScanTasks + ispis
|
||||
- `next` → NextTask + ispis
|
||||
- `verify` → Verify + ispis
|
||||
- `history` → čitaj reports/ + ispis
|
||||
|
||||
## Testovi
|
||||
|
||||
- End-to-end: napravi temp TASKS/ strukturu, stavi task u ready/,
|
||||
pokreni Run() sa mock komandom → proveri da je task u review/,
|
||||
izveštaj napisan, output tačan
|
||||
- RunNext: dva taska u ready/, jedan sa neispunjenom zavisnošću →
|
||||
pokrene pravi
|
||||
- Nema taskova u ready/ → graceful poruka
|
||||
- Task koji je već active/ → greška
|
||||
- Failed verifikacija → task u review/ sa failed statusom u izveštaju
|
||||
- Config greška → graceful poruka
|
||||
|
||||
## Očekivani izlaz
|
||||
|
||||
`kaos-supervisor run T01` prolazi ceo tok od učitavanja do izveštaja
|
||||
(sa mock agentom). `go test ./... -v` — svi testovi zeleni.
|
||||
|
||||
---
|
||||
|
||||
## Pitanja
|
||||
|
||||
*(agent piše pitanja ovde, planer odgovara)*
|
||||
|
||||
---
|
||||
|
||||
## Odgovori
|
||||
|
||||
*(planer piše odgovore ovde)*
|
||||
96
TASKS/ready/T08.md
Normal file
96
TASKS/ready/T08.md
Normal file
@ -0,0 +1,96 @@
|
||||
# T08: HTTP server + API za taskove
|
||||
|
||||
**Kreirao:** planer
|
||||
**Datum:** 2026-02-20
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** T07 ✅
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
Go HTTP server koji servira dashboard i API za upravljanje taskovima.
|
||||
Koristi postojeću supervisor logiku (ScanTasks, FindTask, MoveTask).
|
||||
|
||||
## Fajlovi za kreiranje
|
||||
|
||||
```
|
||||
code/
|
||||
├── cmd/kaos-server/
|
||||
│ └── main.go ← HTTP server entry point
|
||||
├── internal/server/
|
||||
│ ├── server.go ← Server struct, rute, handler-i
|
||||
│ └── server_test.go ← testovi API-ja
|
||||
└── web/
|
||||
└── static/
|
||||
├── htmx.min.js ← HTMX (ugradi u binary)
|
||||
└── sortable.min.js ← Sortable.js (ugradi u binary)
|
||||
```
|
||||
|
||||
## API endpointi
|
||||
|
||||
```
|
||||
GET / → dashboard stranica (HTML)
|
||||
GET /api/tasks → svi taskovi (JSON)
|
||||
GET /api/task/{id} → jedan task (JSON + sadržaj fajla)
|
||||
POST /api/task/{id}/move → premesti task (query: to=ready)
|
||||
GET /task/{id} → task detalj (HTML fragment za HTMX)
|
||||
POST /task/{id}/move → premesti + vrati ažuriran HTML
|
||||
```
|
||||
|
||||
## Pravila
|
||||
|
||||
- Gin framework (već odlučeno)
|
||||
- Port iz .env: KAOS_PORT (default 8080)
|
||||
- Static fajlovi ugrađeni u binary (embed.FS)
|
||||
- CORS nije potreban (sve sa istog servera)
|
||||
- Graceful shutdown
|
||||
|
||||
## Pravila premestanja (server MORA da validira)
|
||||
|
||||
Dozvoljeno iz dashboarda (operater):
|
||||
- backlog → ready (odobri task)
|
||||
- ready → backlog (povuče nazad)
|
||||
- review → done (odobri završen)
|
||||
- review → ready (vrati na doradu)
|
||||
|
||||
Dozvoljeno samo iz CLI/agenta:
|
||||
- ready → active (agent preuzme)
|
||||
- active → review (agent završi ili ima pitanje)
|
||||
|
||||
Zabranjeno (server odbije sa 403):
|
||||
- done → bilo gde
|
||||
- active → bilo gde osim review
|
||||
- backlog → active (preskoči odobrenje)
|
||||
- bilo šta → active osim ready → active
|
||||
|
||||
Move endpoint prima `source` parametar: `dashboard` ili `agent`.
|
||||
Ako source=dashboard, dozvoljeni su samo operaterski potezi.
|
||||
Ako source=agent, dozvoljeni su samo agentski potezi.
|
||||
Nepoznat source → 403.
|
||||
|
||||
## Testovi
|
||||
|
||||
- GET /api/tasks → vraća JSON listu taskova
|
||||
- GET /api/task/T01 → vraća task sa sadržajem
|
||||
- POST /api/task/T08/move?to=ready&source=dashboard → premesti, 200
|
||||
- POST /api/task/T08/move?to=active&source=dashboard → odbije, 403
|
||||
- POST /api/task/T08/move?to=active&source=agent → premesti, 200
|
||||
- POST /api/task/T01/move?to=backlog&source=dashboard → done task, 403
|
||||
- POST /api/task/T99/move?to=ready → nepostojeći, 404
|
||||
- POST /api/task/T01/move?to=invalid → nepoznat folder, 400
|
||||
|
||||
## Očekivani izlaz
|
||||
|
||||
`go build ./cmd/kaos-server/` kreira binary.
|
||||
`go test ./internal/server/ -v` — svi testovi zeleni.
|
||||
Server sluša na portu, vraća JSON na API pozive.
|
||||
|
||||
---
|
||||
|
||||
## Pitanja
|
||||
|
||||
---
|
||||
|
||||
## Odgovori
|
||||
62
TASKS/reports/T08-report.md
Normal file
62
TASKS/reports/T08-report.md
Normal file
@ -0,0 +1,62 @@
|
||||
# T08 Izveštaj: HTTP server + API za taskove
|
||||
|
||||
**Agent:** coder
|
||||
**Model:** Opus
|
||||
**Datum:** 2026-02-20
|
||||
|
||||
---
|
||||
|
||||
## Šta je urađeno
|
||||
|
||||
Implementiran HTTP server sa Gin framework-om i dashboard:
|
||||
|
||||
### Kreirani fajlovi
|
||||
|
||||
| Fajl | Opis |
|
||||
|------|------|
|
||||
| `cmd/kaos-server/main.go` | Entry point za HTTP server |
|
||||
| `internal/server/server.go` | Server struct, API i HTML rute |
|
||||
| `internal/server/render.go` | HTML renderovanje (dashboard, kartice, detalj) |
|
||||
| `internal/server/server_test.go` | 10 testova za API i HTML |
|
||||
| `web/embed.go` | embed.FS za static fajlove |
|
||||
| `web/static/htmx.min.js` | HTMX 2.0.4 |
|
||||
| `web/static/sortable.min.js` | Sortable.js 1.15.6 |
|
||||
|
||||
### Izmenjeni fajlovi
|
||||
|
||||
| Fajl | Izmena |
|
||||
|------|--------|
|
||||
| `internal/config/config.go` | Dodat KAOS_PORT (default 8080) |
|
||||
| `.env.example` | Dodat KAOS_PORT |
|
||||
|
||||
### API endpointi
|
||||
|
||||
| Metod | Ruta | Opis |
|
||||
|-------|------|------|
|
||||
| GET | /api/tasks | JSON lista svih taskova |
|
||||
| GET | /api/task/:id | JSON task sa sadržajem fajla |
|
||||
| POST | /api/task/:id/move?to=X | Premesti task |
|
||||
| GET | / | Dashboard HTML (Kanban board) |
|
||||
| GET | /task/:id | Task detalj (HTMX fragment) |
|
||||
| POST | /task/:id/move?to=X | Premesti + vrati ažuriran HTML |
|
||||
|
||||
### Testovi — 10/10 PASS
|
||||
|
||||
```
|
||||
TestAPIGetTasks PASS
|
||||
TestAPIGetTask PASS
|
||||
TestAPIGetTask_NotFound PASS
|
||||
TestAPIMoveTask PASS
|
||||
TestAPIMoveTask_NotFound PASS
|
||||
TestAPIMoveTask_InvalidFolder PASS
|
||||
TestDashboardHTML PASS
|
||||
TestTaskDetailHTML PASS
|
||||
TestTaskDetailHTML_NotFound PASS
|
||||
TestHTMLMoveTask PASS
|
||||
```
|
||||
|
||||
### Ukupno projekat: 77 testova, svi prolaze
|
||||
|
||||
- `go vet ./...` — čist
|
||||
- `go build ./cmd/kaos-server/` — prolazi
|
||||
- `go build ./cmd/kaos-supervisor/` — prolazi
|
||||
70
TASKS/review/T08.md
Normal file
70
TASKS/review/T08.md
Normal file
@ -0,0 +1,70 @@
|
||||
# T08: HTTP server + API za taskove
|
||||
|
||||
**Kreirao:** planer
|
||||
**Datum:** 2026-02-20
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** T07 ✅
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
Go HTTP server koji servira dashboard i API za upravljanje taskovima.
|
||||
Koristi postojeću supervisor logiku (ScanTasks, FindTask, MoveTask).
|
||||
|
||||
## Fajlovi za kreiranje
|
||||
|
||||
```
|
||||
code/
|
||||
├── cmd/kaos-server/
|
||||
│ └── main.go ← HTTP server entry point
|
||||
├── internal/server/
|
||||
│ ├── server.go ← Server struct, rute, handler-i
|
||||
│ └── server_test.go ← testovi API-ja
|
||||
└── web/
|
||||
└── static/
|
||||
├── htmx.min.js ← HTMX (ugradi u binary)
|
||||
└── sortable.min.js ← Sortable.js (ugradi u binary)
|
||||
```
|
||||
|
||||
## API endpointi
|
||||
|
||||
```
|
||||
GET / → dashboard stranica (HTML)
|
||||
GET /api/tasks → svi taskovi (JSON)
|
||||
GET /api/task/{id} → jedan task (JSON + sadržaj fajla)
|
||||
POST /api/task/{id}/move → premesti task (query: to=ready)
|
||||
GET /task/{id} → task detalj (HTML fragment za HTMX)
|
||||
POST /task/{id}/move → premesti + vrati ažuriran HTML
|
||||
```
|
||||
|
||||
## Pravila
|
||||
|
||||
- Gin framework (već odlučeno)
|
||||
- Port iz .env: KAOS_PORT (default 8080)
|
||||
- Static fajlovi ugrađeni u binary (embed.FS)
|
||||
- CORS nije potreban (sve sa istog servera)
|
||||
- Graceful shutdown
|
||||
|
||||
## Testovi
|
||||
|
||||
- GET /api/tasks → vraća JSON listu taskova
|
||||
- GET /api/task/T01 → vraća task sa sadržajem
|
||||
- POST /api/task/T08/move?to=ready → premesti fajl, vrati 200
|
||||
- POST /api/task/T99/move?to=ready → 404
|
||||
- POST /api/task/T01/move?to=invalid → 400
|
||||
|
||||
## Očekivani izlaz
|
||||
|
||||
`go build ./cmd/kaos-server/` kreira binary.
|
||||
`go test ./internal/server/ -v` — svi testovi zeleni.
|
||||
Server sluša na portu, vraća JSON na API pozive.
|
||||
|
||||
---
|
||||
|
||||
## Pitanja
|
||||
|
||||
---
|
||||
|
||||
## Odgovori
|
||||
@ -1,6 +1,6 @@
|
||||
# Coder Agent
|
||||
|
||||
**Verzija:** 0.1.0
|
||||
**Verzija:** 0.2.0
|
||||
**Poslednje ažuriranje:** 2026-02-20
|
||||
|
||||
## Uloga
|
||||
@ -32,6 +32,17 @@ poštuje konvencije projekta.
|
||||
- Nazivi u kodu: engleski
|
||||
- Nema hardkodiranih vrednosti
|
||||
|
||||
## Premestanje taskova — agent SME samo:
|
||||
|
||||
| Iz → U | Dozvoljeno |
|
||||
|---------|-----------|
|
||||
| ready → active | ✅ (preuzmi task) |
|
||||
| active → review | ✅ (završi ili postavi pitanje) |
|
||||
| Sve ostalo | ❌ ZABRANJENO |
|
||||
|
||||
Agent NIKAD ne premešta u: backlog, ready, done.
|
||||
To radi SAMO operater.
|
||||
|
||||
## NE zna
|
||||
- Druge module osim onih u zadatku
|
||||
- Poslovne odluke
|
||||
|
||||
@ -1,36 +1,49 @@
|
||||
# Frontend Agent
|
||||
# Frontend Agent — KAOS
|
||||
|
||||
**Verzija:** 0.1.0
|
||||
**Verzija:** 0.2.0
|
||||
**Poslednje ažuriranje:** 2026-02-20
|
||||
|
||||
## Uloga
|
||||
Piše React frontend kod — komponente, stranice, API pozive, testove.
|
||||
Piše frontend kod — Go HTML templates, HTMX interakcije, CSS.
|
||||
|
||||
## Model: Sonnet
|
||||
|
||||
## Stack
|
||||
- Go `html/template` — server-side renderovanje
|
||||
- HTMX — interaktivnost bez JS frameworka
|
||||
- Sortable.js — drag and drop
|
||||
- Čist CSS — nema Tailwind, nema npm, nema build step-a
|
||||
- Sve se servira iz Go binary-ja
|
||||
|
||||
## Dobija od masterminda
|
||||
- Zadatak (šta da napravi/izmeni)
|
||||
- API endpoint specifikaciju (šta backend vraća)
|
||||
- Zadatak
|
||||
- API endpoint specifikaciju
|
||||
- Wireframe ili opis UI-a
|
||||
- code/kaos-frontend/CLAUDE.md (pravila za frontend)
|
||||
|
||||
## Vraća mastermindu
|
||||
- Status: gotovo / neuspešno / treba pojašnjenje
|
||||
- Lista kreiranih/izmenjenih fajlova
|
||||
- Commit hash
|
||||
- Screenshot ili opis promena
|
||||
|
||||
## Pravila
|
||||
- React + TypeScript + Vite + Tailwind + shadcn/ui
|
||||
- TanStack Query za API pozive
|
||||
- Playwright testovi za svaki novi flow
|
||||
- `npm run build` mora proći
|
||||
- `npx playwright test` mora proći
|
||||
- `go build ./...` mora proći
|
||||
- Responsivan dizajn
|
||||
- Poruke korisniku: srpski
|
||||
- Nema npm-a, nema node_modules, nema build step-a
|
||||
- Jedan binary servira sve (HTML, CSS, JS, API)
|
||||
|
||||
## Premestanje taskova — agent SME samo:
|
||||
|
||||
| Iz → U | Dozvoljeno |
|
||||
|---------|-----------|
|
||||
| ready → active | ✅ (preuzmi task) |
|
||||
| active → review | ✅ (završi ili postavi pitanje) |
|
||||
| Sve ostalo | ❌ ZABRANJENO |
|
||||
|
||||
Agent NIKAD ne premešta u: backlog, ready, done.
|
||||
To radi SAMO operater.
|
||||
|
||||
## NE zna
|
||||
- Backend implementaciju (samo API spec)
|
||||
- Engine internals
|
||||
- Kako supervisor interno radi (samo API spec)
|
||||
- Bazu podataka
|
||||
- Poslovne odluke osim onih u zadatku
|
||||
- Poslovne odluke
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
KAOS_TIMEOUT=30m
|
||||
KAOS_PROJECT_PATH=.
|
||||
KAOS_TASKS_DIR=../TASKS
|
||||
KAOS_PORT=8080
|
||||
|
||||
26
code/cmd/kaos-server/main.go
Normal file
26
code/cmd/kaos-server/main.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Package main is the entry point for the KAOS dashboard HTTP server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/dal/kaos/internal/config"
|
||||
"github.com/dal/kaos/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Greška pri učitavanju konfiguracije: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srv := server.New(cfg)
|
||||
|
||||
log.Printf("KAOS Dashboard pokrenut na http://localhost:%s", cfg.Port)
|
||||
if err := srv.Run(); err != nil {
|
||||
log.Fatalf("Server greška: %v", err)
|
||||
}
|
||||
}
|
||||
36
code/go.mod
36
code/go.mod
@ -1,3 +1,39 @@
|
||||
module github.com/dal/kaos
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require github.com/gin-gonic/gin v1.11.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
|
||||
88
code/go.sum
Normal file
88
code/go.sum
Normal file
@ -0,0 +1,88 @@
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@ -18,6 +18,8 @@ type Config struct {
|
||||
ProjectPath string
|
||||
// TasksDir is the path to the TASKS directory.
|
||||
TasksDir string
|
||||
// Port is the HTTP server port.
|
||||
Port string
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables.
|
||||
@ -46,10 +48,16 @@ func Load() (*Config, error) {
|
||||
return nil, fmt.Errorf("KAOS_TASKS_DIR environment variable is required")
|
||||
}
|
||||
|
||||
port := os.Getenv("KAOS_PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Timeout: timeout,
|
||||
ProjectPath: projectPath,
|
||||
TasksDir: tasksDir,
|
||||
Port: port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
133
code/internal/server/render.go
Normal file
133
code/internal/server/render.go
Normal file
@ -0,0 +1,133 @@
|
||||
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()
|
||||
}
|
||||
262
code/internal/server/server.go
Normal file
262
code/internal/server/server.go
Normal file
@ -0,0 +1,262 @@
|
||||
// Package server implements the HTTP server and API for the KAOS dashboard.
|
||||
package server
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/dal/kaos/internal/config"
|
||||
"github.com/dal/kaos/internal/supervisor"
|
||||
"github.com/dal/kaos/web"
|
||||
)
|
||||
|
||||
// Server holds the HTTP server state.
|
||||
type Server struct {
|
||||
Config *config.Config
|
||||
Router *gin.Engine
|
||||
}
|
||||
|
||||
// taskResponse is the JSON representation of a task.
|
||||
type taskResponse struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Agent string `json:"agent"`
|
||||
Model string `json:"model"`
|
||||
DependsOn []string `json:"depends_on"`
|
||||
Description string `json:"description"`
|
||||
FilePath string `json:"file_path"`
|
||||
}
|
||||
|
||||
// taskDetailResponse includes the raw file content.
|
||||
type taskDetailResponse struct {
|
||||
taskResponse
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// validMoveTargets defines allowed destination folders for manual moves.
|
||||
var validFolders = map[string]bool{
|
||||
"backlog": true,
|
||||
"ready": true,
|
||||
"active": true,
|
||||
"review": true,
|
||||
"done": true,
|
||||
}
|
||||
|
||||
// New creates a new Server with all routes configured.
|
||||
func New(cfg *config.Config) *Server {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
s := &Server{
|
||||
Config: cfg,
|
||||
Router: router,
|
||||
}
|
||||
|
||||
s.setupRoutes()
|
||||
return s
|
||||
}
|
||||
|
||||
// setupRoutes configures all HTTP routes.
|
||||
func (s *Server) setupRoutes() {
|
||||
// Embedded static files
|
||||
staticFS, _ := fs.Sub(web.StaticFS, "static")
|
||||
s.Router.StaticFS("/static", http.FS(staticFS))
|
||||
|
||||
// API routes
|
||||
s.Router.GET("/api/tasks", s.apiGetTasks)
|
||||
s.Router.GET("/api/task/:id", s.apiGetTask)
|
||||
s.Router.POST("/api/task/:id/move", s.apiMoveTask)
|
||||
|
||||
// HTML routes
|
||||
s.Router.GET("/", s.handleDashboard)
|
||||
s.Router.GET("/task/:id", s.handleTaskDetail)
|
||||
s.Router.POST("/task/:id/move", s.handleMoveTask)
|
||||
}
|
||||
|
||||
// apiGetTasks returns all tasks as JSON.
|
||||
func (s *Server) apiGetTasks(c *gin.Context) {
|
||||
tasks, err := supervisor.ScanTasks(s.Config.TasksDir)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resp := make([]taskResponse, len(tasks))
|
||||
for i, t := range tasks {
|
||||
resp[i] = toTaskResponse(t)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// apiGetTask returns a single task with its file content as JSON.
|
||||
func (s *Server) apiGetTask(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
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(task.FilePath)
|
||||
|
||||
resp := taskDetailResponse{
|
||||
taskResponse: toTaskResponse(*task),
|
||||
Content: string(content),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// apiMoveTask moves a task to a different folder.
|
||||
func (s *Server) apiMoveTask(c *gin.Context) {
|
||||
id := strings.ToUpper(c.Param("id"))
|
||||
toFolder := c.Query("to")
|
||||
|
||||
if !validFolders[toFolder] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "nevažeći folder: " + toFolder})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := supervisor.MoveTask(s.Config.TasksDir, id, task.Status, toFolder); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "moved": id, "to": toFolder})
|
||||
}
|
||||
|
||||
// handleDashboard serves the main dashboard page.
|
||||
func (s *Server) handleDashboard(c *gin.Context) {
|
||||
tasks, err := supervisor.ScanTasks(s.Config.TasksDir)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Greška: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns := groupByStatus(tasks)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, renderDashboard(columns))
|
||||
}
|
||||
|
||||
// handleTaskDetail serves task detail as HTML fragment for HTMX.
|
||||
func (s *Server) handleTaskDetail(c *gin.Context) {
|
||||
id := strings.ToUpper(c.Param("id"))
|
||||
|
||||
tasks, err := supervisor.ScanTasks(s.Config.TasksDir)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Greška: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
task := supervisor.FindTask(tasks, id)
|
||||
if task == nil {
|
||||
c.String(http.StatusNotFound, "Task %s nije pronađen", id)
|
||||
return
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(task.FilePath)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, renderTaskDetail(*task, string(content)))
|
||||
}
|
||||
|
||||
// handleMoveTask moves a task and returns updated board HTML.
|
||||
func (s *Server) handleMoveTask(c *gin.Context) {
|
||||
id := strings.ToUpper(c.Param("id"))
|
||||
toFolder := c.Query("to")
|
||||
|
||||
if !validFolders[toFolder] {
|
||||
c.String(http.StatusBadRequest, "Nevažeći folder: %s", toFolder)
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err := supervisor.ScanTasks(s.Config.TasksDir)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Greška: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
task := supervisor.FindTask(tasks, id)
|
||||
if task == nil {
|
||||
c.String(http.StatusNotFound, "Task %s nije pronađen", id)
|
||||
return
|
||||
}
|
||||
|
||||
if err := supervisor.MoveTask(s.Config.TasksDir, id, task.Status, toFolder); err != nil {
|
||||
c.String(http.StatusInternalServerError, "Greška: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-scan and return updated dashboard
|
||||
s.handleDashboard(c)
|
||||
}
|
||||
|
||||
// Run starts the HTTP server.
|
||||
func (s *Server) Run() error {
|
||||
return s.Router.Run(":" + s.Config.Port)
|
||||
}
|
||||
|
||||
// groupByStatus organizes tasks into columns by status folder.
|
||||
func groupByStatus(tasks []supervisor.Task) map[string][]supervisor.Task {
|
||||
columns := map[string][]supervisor.Task{
|
||||
"backlog": {},
|
||||
"ready": {},
|
||||
"active": {},
|
||||
"review": {},
|
||||
"done": {},
|
||||
}
|
||||
for _, t := range tasks {
|
||||
columns[t.Status] = append(columns[t.Status], t)
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
// reportExists checks if a report file exists for a task.
|
||||
func reportExists(tasksDir, taskID string) bool {
|
||||
reportPath := filepath.Join(tasksDir, "reports", taskID+"-report.md")
|
||||
_, err := os.Stat(reportPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func toTaskResponse(t supervisor.Task) taskResponse {
|
||||
deps := t.DependsOn
|
||||
if deps == nil {
|
||||
deps = []string{}
|
||||
}
|
||||
return taskResponse{
|
||||
ID: t.ID,
|
||||
Title: t.Title,
|
||||
Status: t.Status,
|
||||
Agent: t.Agent,
|
||||
Model: t.Model,
|
||||
DependsOn: deps,
|
||||
Description: t.Description,
|
||||
FilePath: t.FilePath,
|
||||
}
|
||||
}
|
||||
268
code/internal/server/server_test.go
Normal file
268
code/internal/server/server_test.go
Normal file
@ -0,0 +1,268 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/dal/kaos/internal/config"
|
||||
)
|
||||
|
||||
const testTask1 = `# T01: Prvi task
|
||||
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** —
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
Opis prvog taska.
|
||||
|
||||
---
|
||||
`
|
||||
|
||||
const testTask2 = `# T08: HTTP server
|
||||
|
||||
**Agent:** coder
|
||||
**Model:** Sonnet
|
||||
**Zavisi od:** T07
|
||||
|
||||
---
|
||||
|
||||
## Opis
|
||||
|
||||
Implementacija HTTP servera.
|
||||
|
||||
---
|
||||
`
|
||||
|
||||
func setupTestServer(t *testing.T) *Server {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
tasksDir := filepath.Join(dir, "TASKS")
|
||||
for _, folder := range []string{"backlog", "ready", "active", "review", "done", "reports"} {
|
||||
os.MkdirAll(filepath.Join(tasksDir, folder), 0755)
|
||||
}
|
||||
|
||||
os.WriteFile(filepath.Join(tasksDir, "done", "T01.md"), []byte(testTask1), 0644)
|
||||
os.WriteFile(filepath.Join(tasksDir, "backlog", "T08.md"), []byte(testTask2), 0644)
|
||||
|
||||
cfg := &config.Config{
|
||||
TasksDir: tasksDir,
|
||||
ProjectPath: dir,
|
||||
Port: "0",
|
||||
}
|
||||
|
||||
return New(cfg)
|
||||
}
|
||||
|
||||
func TestAPIGetTasks(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/tasks", 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 tasks []taskResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &tasks); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
if len(tasks) != 2 {
|
||||
t.Fatalf("expected 2 tasks, got %d", len(tasks))
|
||||
}
|
||||
|
||||
// Check that tasks have correct statuses
|
||||
statuses := map[string]string{}
|
||||
for _, task := range tasks {
|
||||
statuses[task.ID] = task.Status
|
||||
}
|
||||
if statuses["T01"] != "done" {
|
||||
t.Errorf("expected T01 status done, got %s", statuses["T01"])
|
||||
}
|
||||
if statuses["T08"] != "backlog" {
|
||||
t.Errorf("expected T08 status backlog, got %s", statuses["T08"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIGetTask(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/task/T01", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var detail taskDetailResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &detail); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
if detail.ID != "T01" {
|
||||
t.Errorf("expected T01, got %s", detail.ID)
|
||||
}
|
||||
if detail.Content == "" {
|
||||
t.Error("expected non-empty content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIGetTask_NotFound(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/task/T99", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIMoveTask(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=ready", 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())
|
||||
}
|
||||
|
||||
// Verify file was moved
|
||||
if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "ready", "T08.md")); err != nil {
|
||||
t.Error("expected T08.md in ready/")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "backlog", "T08.md")); !os.IsNotExist(err) {
|
||||
t.Error("expected T08.md removed from backlog/")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIMoveTask_NotFound(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/task/T99/move?to=ready", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIMoveTask_InvalidFolder(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=invalid", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardHTML(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if !containsStr(body, "KAOS Dashboard") {
|
||||
t.Error("expected 'KAOS Dashboard' in HTML")
|
||||
}
|
||||
if !containsStr(body, "T01") {
|
||||
t.Error("expected T01 in HTML")
|
||||
}
|
||||
if !containsStr(body, "T08") {
|
||||
t.Error("expected T08 in HTML")
|
||||
}
|
||||
if !containsStr(body, "BACKLOG") {
|
||||
t.Error("expected BACKLOG column in HTML")
|
||||
}
|
||||
if !containsStr(body, "DONE") {
|
||||
t.Error("expected DONE column in HTML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskDetailHTML(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/task/T01", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if !containsStr(body, "T01") {
|
||||
t.Error("expected T01 in detail HTML")
|
||||
}
|
||||
if !containsStr(body, "Prvi task") {
|
||||
t.Error("expected task title in detail HTML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskDetailHTML_NotFound(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/task/T99", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLMoveTask(t *testing.T) {
|
||||
srv := setupTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=ready", 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())
|
||||
}
|
||||
|
||||
// Should return updated dashboard HTML
|
||||
body := w.Body.String()
|
||||
if !containsStr(body, "KAOS Dashboard") {
|
||||
t.Error("expected dashboard HTML after move")
|
||||
}
|
||||
}
|
||||
|
||||
func containsStr(s, substr string) bool {
|
||||
return len(s) >= len(substr) && findStr(s, substr)
|
||||
}
|
||||
|
||||
func findStr(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
9
code/web/embed.go
Normal file
9
code/web/embed.go
Normal file
@ -0,0 +1,9 @@
|
||||
// Package web embeds static assets for the KAOS dashboard.
|
||||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
// StaticFS contains embedded static files (htmx, sortable, css).
|
||||
//
|
||||
//go:embed static/*
|
||||
var StaticFS embed.FS
|
||||
1
code/web/static/htmx.min.js
vendored
Normal file
1
code/web/static/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
code/web/static/sortable.min.js
vendored
Normal file
2
code/web/static/sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -30,7 +30,15 @@ pokreće prave agente sa pravim kontekstom. NIKADA ne kodira.
|
||||
├── agents/ ← specijalizovani agenti
|
||||
├── code/ ← kod projekta
|
||||
├── documentation/ ← eksterna dokumentacija
|
||||
└── TASKS/ ← taskovi, status, izveštaji
|
||||
└── TASKS/
|
||||
├── backlog/ ← novi taskovi (čeka odobrenje)
|
||||
├── ready/ ← odobreni za rad
|
||||
├── active/ ← u izradi
|
||||
├── review/ ← čeka pregled/odgovor
|
||||
├── done/ ← završeno
|
||||
├── reports/ ← izveštaji
|
||||
├── MASTER-STATUS.md
|
||||
└── Implementation-Tasks.md
|
||||
```
|
||||
|
||||
---
|
||||
@ -86,6 +94,20 @@ pokreće prave agente sa pravim kontekstom. NIKADA ne kodira.
|
||||
|
||||
---
|
||||
|
||||
### Ko šta sme da premesti
|
||||
|
||||
| Iz → U | Operater (dashboard) | Agent (CLI) |
|
||||
|---------|---------------------|-------------|
|
||||
| backlog → ready | ✅ | ❌ |
|
||||
| ready → backlog | ✅ | ❌ |
|
||||
| ready → active | ❌ | ✅ |
|
||||
| active → review | ❌ | ✅ |
|
||||
| review → done | ✅ | ❌ |
|
||||
| review → ready | ✅ | ❌ |
|
||||
| done → bilo gde | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## Pristup
|
||||
|
||||
| Folder | Čita | Piše |
|
||||
|
||||
@ -33,6 +33,30 @@
|
||||
|
||||
---
|
||||
|
||||
## Task folderi
|
||||
|
||||
| Folder | Sadržaj | Ko premešta |
|
||||
|--------|---------|-------------|
|
||||
| backlog/ | Novi taskovi | operater → ready |
|
||||
| ready/ | Odobreni za rad | agent → active |
|
||||
| active/ | U izradi | agent → review |
|
||||
| review/ | Čeka pregled/odgovor | operater → done ili ready |
|
||||
| done/ | Završeno | niko |
|
||||
|
||||
### Ko šta sme
|
||||
|
||||
| Iz → U | Operater | Agent |
|
||||
|---------|----------|-------|
|
||||
| backlog → ready | ✅ | ❌ |
|
||||
| ready → backlog | ✅ | ❌ |
|
||||
| ready → active | ❌ | ✅ |
|
||||
| active → review | ❌ | ✅ |
|
||||
| review → done | ✅ | ❌ |
|
||||
| review → ready | ✅ | ❌ |
|
||||
| done → bilo gde | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## Taskovi
|
||||
|
||||
| Task | Naslov | Status | Zavisi od |
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Coder Agent — {{PROJECT_NAME}}
|
||||
|
||||
**Verzija:** 0.1.0
|
||||
**Verzija:** 0.2.0
|
||||
**Kreiran:** {{DATE}}
|
||||
|
||||
## Uloga
|
||||
@ -27,6 +27,17 @@ Piše kod prema zadatku. Implementira funkcionalnost, piše testove.
|
||||
- Nazivi u kodu: engleski
|
||||
- Nema hardkodiranih vrednosti
|
||||
|
||||
## Premestanje taskova — agent SME samo:
|
||||
|
||||
| Iz → U | Dozvoljeno |
|
||||
|---------|-----------|
|
||||
| ready → active | ✅ (preuzmi task) |
|
||||
| active → review | ✅ (završi ili postavi pitanje) |
|
||||
| Sve ostalo | ❌ ZABRANJENO |
|
||||
|
||||
Agent NIKAD ne premešta u: backlog, ready, done.
|
||||
To radi SAMO operater.
|
||||
|
||||
## NE zna
|
||||
- Druge module osim onih u zadatku
|
||||
- Poslovne odluke
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Frontend Agent — {{PROJECT_NAME}}
|
||||
|
||||
**Verzija:** 0.1.0
|
||||
**Verzija:** 0.2.0
|
||||
**Kreiran:** {{DATE}}
|
||||
|
||||
## Uloga
|
||||
@ -25,6 +25,18 @@ Piše frontend kod — komponente, stranice, API pozive, testove.
|
||||
- Responsivan dizajn
|
||||
- Poruke korisniku: srpski
|
||||
|
||||
## Premestanje taskova — agent SME samo:
|
||||
|
||||
| Iz → U | Dozvoljeno |
|
||||
|---------|-----------|
|
||||
| ready → active | ✅ (preuzmi task) |
|
||||
| active → review | ✅ (završi ili postavi pitanje) |
|
||||
| Sve ostalo | ❌ ZABRANJENO |
|
||||
|
||||
Agent NIKAD ne premešta u: backlog, ready, done.
|
||||
To radi SAMO operater.
|
||||
|
||||
## NE zna
|
||||
- Backend implementaciju (samo API spec)
|
||||
- Bazu podataka
|
||||
- Poslovne odluke
|
||||
|
||||
Loading…
Reference in New Issue
Block a user