chore: auto-commit (5 archivos)
- app.md - backend/chat.go - Dockerfile - docker-compose.yml - traefik-dynamic.yml Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
FROM golang:1.25-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc g++ libsqlite3-dev pkg-config ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
WORKDIR /build/apps/kanban/backend
|
||||||
|
RUN CGO_ENABLED=1 go build -ldflags='-s -w' -o /kanban .
|
||||||
|
|
||||||
|
# --- Runtime ---
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libsqlite3-0 libstdc++6 ca-certificates tzdata \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /kanban /usr/local/bin/kanban
|
||||||
|
|
||||||
|
WORKDIR /data
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
EXPOSE 8095
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/kanban"]
|
||||||
|
CMD ["--port", "8095", "--db", "/data/operations.db"]
|
||||||
@@ -43,6 +43,32 @@ uses_types:
|
|||||||
framework: "net/http + vite + react + mantine + dnd-kit"
|
framework: "net/http + vite + react + mantine + dnd-kit"
|
||||||
entry_point: "backend/main.go"
|
entry_point: "backend/main.go"
|
||||||
dir_path: "apps/kanban"
|
dir_path: "apps/kanban"
|
||||||
|
|
||||||
|
# Validacion end-to-end (fase 4 del bucle reactivo). Ver issue 0068.
|
||||||
|
e2e_checks:
|
||||||
|
- id: build_frontend
|
||||||
|
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
|
||||||
|
timeout_s: 180
|
||||||
|
expect_exit: 0
|
||||||
|
- id: build_backend
|
||||||
|
cmd: "CGO_ENABLED=1 go build -tags fts5 -o kanban ."
|
||||||
|
timeout_s: 120
|
||||||
|
expect_exit: 0
|
||||||
|
- id: migrations_apply
|
||||||
|
cmd: "rm -f /tmp/kanban_e2e.db && ./kanban --port 0 --db /tmp/kanban_e2e.db --migrate-only"
|
||||||
|
timeout_s: 15
|
||||||
|
expect_exit: 0
|
||||||
|
- id: migrations_schema
|
||||||
|
cmd: "sqlite3 /tmp/kanban_e2e.db 'SELECT version FROM schema_migrations ORDER BY version;'"
|
||||||
|
expect_stdout_contains: "1"
|
||||||
|
- id: smoke_api
|
||||||
|
cmd: "./kanban --port 8195 --db /tmp/kanban_e2e.db &"
|
||||||
|
health: "http://127.0.0.1:8195/api/board"
|
||||||
|
timeout_s: 10
|
||||||
|
- id: tests_go
|
||||||
|
cmd: "go test -tags fts5 -count=1 ./..."
|
||||||
|
timeout_s: 120
|
||||||
|
expect_exit: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
## Arquitectura
|
## Arquitectura
|
||||||
|
|||||||
+38
-9
@@ -15,18 +15,17 @@ import (
|
|||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
const chatSystemPrompt = `Eres el asistente del tablero kanban. Responde al usuario y, cuando pida cambios, modifica el tablero llamando a tools nativas (MCP).
|
const chatSystemPrompt = `Asistente del tablero kanban. Modifica el tablero llamando a tools MCP cuando el usuario pida cambios. Responde texto en markdown cuando solo informe.
|
||||||
|
|
||||||
Tools disponibles via MCP server "kanban":
|
Tools (MCP server "kanban"):
|
||||||
- list_board / find_cards / card_history / list_users — lectura
|
- Lectura: list_board, find_cards, card_history, list_users
|
||||||
- create_column / update_column / delete_column / reorder_columns — columnas
|
- Columnas: create_column, update_column, delete_column, reorder_columns
|
||||||
- create_card / update_card / delete_card / move_card / assign_card — tarjetas
|
- Tarjetas: create_card, update_card, delete_card, move_card, assign_card
|
||||||
|
|
||||||
Llama directamente a las tools cuando necesites mutar el tablero. Usa list_board al principio si necesitas resolver nombres a IDs. NUNCA inventes IDs.
|
El estado actual del tablero viene en <board_state> al final del mensaje. Usa esos IDs directamente — NO llames list_board si ya tienes lo que necesitas. NUNCA inventes IDs.
|
||||||
|
|
||||||
Cuando termines, responde texto natural en markdown (sin llamadas extra) — eso señala el fin de la conversacion.`
|
Cuando termines, responde texto natural sin mas llamadas — eso cierra la conversacion.`
|
||||||
|
|
||||||
const claudeModel = "claude-sonnet-4-6"
|
|
||||||
const claudeTimeout = 300 * time.Second
|
const claudeTimeout = 300 * time.Second
|
||||||
|
|
||||||
func claudeBinary() string {
|
func claudeBinary() string {
|
||||||
@@ -36,6 +35,13 @@ func claudeBinary() string {
|
|||||||
return "claude"
|
return "claude"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func claudeModel() string {
|
||||||
|
if m := os.Getenv("KANBAN_CLAUDE_MODEL"); m != "" {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return "claude-haiku-4-5-20251001"
|
||||||
|
}
|
||||||
|
|
||||||
type chatMessage struct {
|
type chatMessage struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
@@ -125,13 +131,18 @@ func streamChat(ctx context.Context, conn *websocket.Conn, db *DB, workdir, toke
|
|||||||
defer os.Remove(mcpPath)
|
defer os.Remove(mcpPath)
|
||||||
|
|
||||||
prompt := flattenMessages(msgs)
|
prompt := flattenMessages(msgs)
|
||||||
|
if board, err := boardSnapshot(db); err == nil && board != "" {
|
||||||
|
prompt += "\n\n<board_state>\n" + board + "\n</board_state>\n"
|
||||||
|
}
|
||||||
|
|
||||||
stdin := strings.NewReader(prompt)
|
stdin := strings.NewReader(prompt)
|
||||||
events, err := core.StreamClaude(ctx, core.ClaudeStreamOpts{
|
events, err := core.StreamClaude(ctx, core.ClaudeStreamOpts{
|
||||||
Bin: claudeBinary(),
|
Bin: claudeBinary(),
|
||||||
Args: []string{
|
Args: []string{
|
||||||
"--model", claudeModel,
|
"--model", claudeModel(),
|
||||||
|
"--no-session-persistence",
|
||||||
"--mcp-config", mcpPath,
|
"--mcp-config", mcpPath,
|
||||||
|
"--strict-mcp-config",
|
||||||
"--system-prompt", chatSystemPrompt,
|
"--system-prompt", chatSystemPrompt,
|
||||||
"--allowedTools",
|
"--allowedTools",
|
||||||
"mcp__kanban__list_board,mcp__kanban__create_column,mcp__kanban__update_column,mcp__kanban__rename_column,mcp__kanban__delete_column,mcp__kanban__reorder_columns,mcp__kanban__create_card,mcp__kanban__update_card,mcp__kanban__delete_card,mcp__kanban__move_card,mcp__kanban__card_history,mcp__kanban__find_cards,mcp__kanban__list_users,mcp__kanban__assign_card",
|
"mcp__kanban__list_board,mcp__kanban__create_column,mcp__kanban__update_column,mcp__kanban__rename_column,mcp__kanban__delete_column,mcp__kanban__reorder_columns,mcp__kanban__create_card,mcp__kanban__update_card,mcp__kanban__delete_card,mcp__kanban__move_card,mcp__kanban__card_history,mcp__kanban__find_cards,mcp__kanban__list_users,mcp__kanban__assign_card",
|
||||||
@@ -215,6 +226,24 @@ func flattenMessages(msgs []chatMessage) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// boardSnapshot returns a JSON dump of columns + cards to inject in the
|
||||||
|
// initial prompt, saving a list_board round-trip.
|
||||||
|
func boardSnapshot(db *DB) (string, error) {
|
||||||
|
cols, err := db.ListColumns()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cards, err := db.ListCardsWithTime()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(map[string]any{"columns": cols, "cards": cards})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
// chatWorkdir resolves an absolute working directory for `claude -p`.
|
// chatWorkdir resolves an absolute working directory for `claude -p`.
|
||||||
func chatWorkdir(dbPath string) string {
|
func chatWorkdir(dbPath string) string {
|
||||||
abs, err := filepath.Abs(dbPath)
|
abs, err := filepath.Abs(dbPath)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: kanban
|
||||||
|
|
||||||
|
services:
|
||||||
|
kanban:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: apps/kanban/Dockerfile
|
||||||
|
container_name: kanban
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8095:8095"
|
||||||
|
volumes:
|
||||||
|
- kanban_data:/data
|
||||||
|
networks:
|
||||||
|
- coolify
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
kanban_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
coolify:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
kanban-http:
|
||||||
|
rule: "Host(`kanban.organic-machine.com`)"
|
||||||
|
entryPoints:
|
||||||
|
- "http"
|
||||||
|
middlewares:
|
||||||
|
- "kanban-redirect"
|
||||||
|
service: "kanban-service"
|
||||||
|
|
||||||
|
kanban-https:
|
||||||
|
rule: "Host(`kanban.organic-machine.com`)"
|
||||||
|
entryPoints:
|
||||||
|
- "https"
|
||||||
|
middlewares:
|
||||||
|
- "kanban-auth"
|
||||||
|
- "kanban-gzip"
|
||||||
|
service: "kanban-service"
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
services:
|
||||||
|
kanban-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://kanban:8095"
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
kanban-redirect:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: "https"
|
||||||
|
kanban-auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "lucas:$2y$05$67qHI0i2NiSbVC40gJvqHOb28PKgkNiKYtsdJEEgw3FxT4j4NQcrG"
|
||||||
|
kanban-gzip:
|
||||||
|
compress: true
|
||||||
Reference in New Issue
Block a user