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:
2026-05-09 18:11:24 +02:00
parent ce807ec2ee
commit f1ee116d3b
5 changed files with 151 additions and 9 deletions
+28
View File
@@ -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"]
+26
View File
@@ -43,6 +43,32 @@ uses_types:
framework: "net/http + vite + react + mantine + dnd-kit"
entry_point: "backend/main.go"
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
+38 -9
View File
@@ -15,18 +15,17 @@ import (
"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":
- list_board / find_cards / card_history / list_users — lectura
- create_column / update_column / delete_column / reorder_columns — columnas
- create_card / update_card / delete_card / move_card / assign_card — tarjetas
Tools (MCP server "kanban"):
- Lectura: list_board, find_cards, card_history, list_users
- Columnas: create_column, update_column, delete_column, reorder_columns
- 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
func claudeBinary() string {
@@ -36,6 +35,13 @@ func claudeBinary() string {
return "claude"
}
func claudeModel() string {
if m := os.Getenv("KANBAN_CLAUDE_MODEL"); m != "" {
return m
}
return "claude-haiku-4-5-20251001"
}
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
@@ -125,13 +131,18 @@ func streamChat(ctx context.Context, conn *websocket.Conn, db *DB, workdir, toke
defer os.Remove(mcpPath)
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)
events, err := core.StreamClaude(ctx, core.ClaudeStreamOpts{
Bin: claudeBinary(),
Args: []string{
"--model", claudeModel,
"--model", claudeModel(),
"--no-session-persistence",
"--mcp-config", mcpPath,
"--strict-mcp-config",
"--system-prompt", chatSystemPrompt,
"--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",
@@ -215,6 +226,24 @@ func flattenMessages(msgs []chatMessage) 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`.
func chatWorkdir(dbPath string) string {
abs, err := filepath.Abs(dbPath)
+22
View File
@@ -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
+37
View File
@@ -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