466a055f72
- app.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
201 lines
12 KiB
Markdown
201 lines
12 KiB
Markdown
---
|
|
name: kanban
|
|
lang: go
|
|
domain: tools
|
|
version: 0.5.2
|
|
description: "Kanban board con persistencia SQLite, drag-and-drop entre columnas (dnd-kit), tracking del tiempo por columna, adjuntos de archivos por card, notificaciones realtime (SSE) y modulos externos (Jira). Frontend Vite + React + Mantine v9 embebido en el binario Go. Endpoint MCP Streamable HTTP en /mcp."
|
|
tags: [service, kanban, web, dnd-kit, mantine, sqlite, time-tracking]
|
|
uses_functions:
|
|
- random_hex_id_go_core
|
|
- parse_date_or_default_go_core
|
|
- sqlite_open_go_infra
|
|
- sqlite_apply_migrations_go_infra
|
|
- sqlite_column_exists_go_infra
|
|
- spa_handler_go_infra
|
|
- http_router_go_infra
|
|
- http_serve_go_infra
|
|
- http_middleware_chain_go_infra
|
|
- http_cors_middleware_go_infra
|
|
- http_logger_middleware_go_infra
|
|
- http_json_response_go_infra
|
|
- http_error_response_go_infra
|
|
- http_parse_body_go_infra
|
|
- http_session_cookie_middleware_go_infra
|
|
- http_session_token_extract_go_infra
|
|
- http_session_cookie_set_go_infra
|
|
- http_session_cookie_clear_go_infra
|
|
- password_hash_go_infra
|
|
- password_verify_go_infra
|
|
- session_create_go_infra
|
|
- session_cleanup_go_infra
|
|
- duration_stats_go_datascience
|
|
- format_duration_ts_core
|
|
- format_datetime_short_ts_core
|
|
- string_hash_palette_ts_core
|
|
- color_bg_ts_ui
|
|
- color_border_ts_ui
|
|
- color_swatch_ts_ui
|
|
- fetch_json_ts_infra
|
|
- claude_stream_go_core
|
|
- mcp_server_stdio_go_infra
|
|
- mcp_server_http_go_infra
|
|
- ws_upgrader_go_infra
|
|
uses_types:
|
|
- DurationStats_go_datascience
|
|
framework: "net/http + vite + react + mantine + dnd-kit"
|
|
entry_point: "backend/main.go"
|
|
dir_path: "apps/kanban"
|
|
service:
|
|
port: 8095
|
|
health_endpoint: /api/board
|
|
health_timeout_s: 3
|
|
systemd_unit: kanban.service
|
|
systemd_scope: user
|
|
restart_policy: always
|
|
runtime: systemd-user
|
|
pc_targets:
|
|
- aurgi-pc
|
|
is_local_only: false
|
|
|
|
# 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
|
|
- id: smoke_files
|
|
cmd: "bash e2e/files_smoke.sh"
|
|
timeout_s: 30
|
|
expect_exit: 0
|
|
---
|
|
|
|
## Arquitectura
|
|
|
|
Single-binary: backend Go con frontend Vite embebido. SQLite local con tres tablas (`columns`, `cards`, `card_column_history`) y endpoints REST.
|
|
|
|
```
|
|
./kanban --port 8095 --db kanban.db
|
|
```
|
|
|
|
### Schema SQLite (`migrations/001_init.sql` … `010_card_messages.sql`)
|
|
|
|
- **columns** — id, name, position, created_at
|
|
- **cards** — id, title, description, column_id (FK), position, created_at, updated_at
|
|
- **card_column_history** — id, card_id (FK), column_id, entered_at, exited_at
|
|
- Una entrada con `exited_at IS NULL` = posicion actual
|
|
- Al mover una tarjeta a otra columna: cierra la entrada activa (`exited_at = now`) e inserta una nueva
|
|
- El borrado de tarjeta hace CASCADE sobre el historial
|
|
- **card_messages** (migration 010) — id, card_id (FK CASCADE), author_id (nullable), body, created_at. Comentarios humano-a-humano por card; distintos de `card_events` (sistema) y `/api/chat` (LLM global).
|
|
|
|
### API REST
|
|
|
|
| Metodo | Path | Cuerpo |
|
|
|---|---|---|
|
|
| GET | `/api/board` | — (retorna `{columns, cards}`, cada card incluye `time_in_column_ms`) |
|
|
| POST | `/api/columns` | `{name}` |
|
|
| PATCH | `/api/columns/{id}` | `{name?, position?}` |
|
|
| DELETE | `/api/columns/{id}` | — (cascade a cards) |
|
|
| POST | `/api/columns/reorder` | `{ids: [...]}` |
|
|
| POST | `/api/cards` | `{column_id, title, description?}` |
|
|
| PATCH | `/api/cards/{id}` | `{title?, description?}` |
|
|
| DELETE | `/api/cards/{id}` | — |
|
|
| POST | `/api/cards/{id}/move` | `{column_id, ordered_ids: [...]}` |
|
|
| POST | `/api/cards/{id}/duplicate` | — (clona la card en la misma columna al final; copia titulo+" (copia)", descripcion, color, requester, assignee, tags, stickers, deadline; NO copia historial ni mensajes) |
|
|
| GET | `/api/cards/{id}/messages` | — (lista de comentarios humano-a-humano de la card) |
|
|
| POST | `/api/cards/{id}/messages` | `{body}` (crea comentario; author = usuario de la sesion) |
|
|
| DELETE | `/api/cards/{cid}/messages/{mid}` | — (solo el autor puede borrar su mensaje) |
|
|
| GET | `/api/cards/{id}/history` | — (timeline con duraciones por columna) |
|
|
| GET | `/api/flags` | — (retorna `{ <name>: bool }` con los feature flags efectivos en esta instancia) |
|
|
| POST | `/api/auth/register` | `{username, password, display_name?}` (devuelve 403 `registration_disabled` si el flag `registration-enabled` esta en `false`) |
|
|
|
|
### Feature flags
|
|
|
|
`dev/feature_flags.json` (lado del repo) define los flags por instancia. Se cargan al arrancar (override con `--flags <path>`); fichero ausente equivale a "todos los flags en `false`". El endpoint `GET /api/flags` expone el estado actual para que el frontend oculte UI condicional (ej. el toggle de "Registrate" en `LoginPage` solo aparece cuando `registration-enabled` es `true`).
|
|
|
|
| Flag | Default | Efecto cuando esta en `true` |
|
|
|---|---|---|
|
|
| `registration-enabled` | `false` | Permite crear cuentas nuevas via `POST /api/auth/register` y muestra el toggle "Registrate" en la pantalla de login. |
|
|
|
|
### Frontend
|
|
|
|
- **dnd-kit** (`@dnd-kit/core` + `@dnd-kit/sortable`) para drag-and-drop entre y dentro de columnas (multi-container).
|
|
- **Mantine v9** + `@tabler/icons-react` para UI.
|
|
- **Modales** con `@mantine/modals` (confirmacion borrado, history timeline).
|
|
- Time-in-column live: `time_in_column_ms` del backend + tick local cada segundo para que el badge se actualice sin reload.
|
|
- DnD con `closestCorners` + `DragOverlay` para feedback visual al arrastrar.
|
|
- **Auto-refresh:** el board se recarga cada 30s (`api.getBoard`) sin interaccion del usuario; equivalente a pulsar el boton de refresco. El tick de 1s del time-in-column es independiente y no toca red.
|
|
- **Modal de card en dos columnas** (`CardEditPanel`): izquierda mantiene `CardForm` (titulo, solicitante, descripcion, asignacion, tags); derecha es un `Tabs` con `Chat` (por defecto) | `Enlaces` | `Archivos` (proximamente). Tamaño del modal: 85% del viewport.
|
|
- **Chat per-card** (`CardChatPanel`): lista de comentarios humano-a-humano persistidos en `card_messages`. Enter envia, Shift+Enter salto de linea. Solo el autor puede borrar su propio mensaje.
|
|
- **Enlaces** (`CardLinksPanel`): extrae URLs (`https?://...`) de titulo, descripcion y cuerpo de cada mensaje del chat. Deduplica, muestra hostname + URL completa + badge de origen. Click abre en pestaña nueva (`target="_blank"`).
|
|
- **Duplicar card:** click derecho sobre la card abre el menu contextual (mismo que el boton `⋮`), donde aparece el item "Duplicar". Al pulsarlo invoca `POST /api/cards/{id}/duplicate`. La copia se inserta al final de la misma columna con titulo + " (copia)".
|
|
- **Sesion obligatoria para chat:** `POST/DELETE /api/cards/{id}/messages` exige sesion activa (401 si falta). `author_id` siempre poblado; no hay comentarios anonimos.
|
|
- **Archivos** (`CardFilesPanel`): adjuntos por card almacenados en `apps/kanban/uploads/<card_id>/<random>__<safe_filename>` (filesystem, gitignored). Tabla `card_files` con soft-delete. Limite 10 MB por archivo. Tres vias de upload:
|
|
1. Drag&drop en el editor de descripcion (`CardForm`) → inserta `` (imagen) o `[name](url)` (resto) en la posicion del cursor.
|
|
2. Drag&drop o boton paperclip en el chat (`CardChatPanel`) → crea un mensaje cuyo cuerpo es la ref markdown.
|
|
3. Boton "Subir" en el tab Archivos → sube sin embed.
|
|
- El renderer de mensajes (`MessageBody`) reconoce `` -> `<Image>` thumb 220px y `[name](url)` -> `<Anchor>`. Texto plano se renderiza con `whiteSpace: pre-wrap`.
|
|
- Endpoints: `POST /api/cards/{id}/files` (multipart, 10 MB max), `GET /api/cards/{id}/files`, `GET /api/files/{id}` (sirve binario con `inline` o `attachment` segun MIME), `DELETE /api/files/{id}` (soft delete).
|
|
|
|
### Build
|
|
|
|
```bash
|
|
cd frontend && pnpm install && pnpm build
|
|
cd .. && CGO_ENABLED=1 go build -tags fts5 -o kanban .
|
|
./kanban --port 8095 --db kanban.db
|
|
# Browser: http://localhost:8095
|
|
```
|
|
|
|
### Dev (frontend con HMR contra backend)
|
|
|
|
```bash
|
|
# Terminal 1
|
|
./kanban --port 8095 --db kanban.db
|
|
|
|
# Terminal 2
|
|
cd frontend && pnpm dev
|
|
# Browser: http://localhost:5180 (vite proxy /api → 8095)
|
|
```
|
|
|
|
## Notas
|
|
|
|
- Puerto por defecto 8095.
|
|
- DB por defecto `kanban.db` en cwd.
|
|
- IDs de columnas y tarjetas: 16 chars hex (8 bytes random) via `random_hex_id_go_core`.
|
|
- El historial conserva la cronologia exacta — incluso despues de cerrar y reabrir el server, los tiempos vivos siguen contando desde `entered_at`.
|
|
- El borrado de columna hace CASCADE: las tarjetas se borran y su historial tambien. Si se quiere preservar el historial al borrar, deberia archivarse en lugar de borrar.
|
|
|
|
|
|
## Capability growth log
|
|
|
|
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
|
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
|
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
|
- `patch`: bugfix sin cambio observable.
|
|
|
|
- v0.1.0 (2026-05-18) — baseline.
|
|
- v0.2.0 (2026-05-27) — adjuntos de archivos por card (issue 0128): tabla `card_files` con soft-delete, endpoints REST (`POST/GET/DELETE /api/cards/{id}/files`, `GET/DELETE /api/files/{id}`), tres vias de upload (drag&drop en descripcion y chat, boton en tab Archivos), render inline de imagenes via `MessageBody`. Limite 10 MB.
|
|
- v0.3.1 (2026-05-21) — patch: debounce board.invalidated (300ms trailing) + autoClose 4s en toasts de notification.created. Fix de blow-up de memoria en navegador por ráfagas de SSE.
|
|
- v0.4.0 (2026-05-22) — minor: endpoint MCP Streamable HTTP `/mcp` con per-user bearer tokens (tabla `mcp_tokens`, migration 017). Modal "MCP tokens" en avatar menu para generar/listar/revocar. Vite proxy enruta `/mcp` a WSL. Usa nueva funcion `mcp_server_http_go_infra`. Doc en `docs/MCP.md`.
|
|
- v0.5.2 (2026-06-01) — patch: el alta a Jira rellena el campo obligatorio "Área Solicitante" (`customfield_10158`) que el issue type Epic (y Mejora) del proyecto DATA exige en la pantalla de creacion. Sin esto, el `card.created` del 0.5.1 daba HTTP 400 "Solicitante is required". Nuevos campos en `jiraConfig`: `requester_field`, `requester_map`, `requester_default`. `create()`/`update()` inyectan el campo como single-select `{value:<opcion>}` resuelto desde el requester de la card (mapa case-insensitive) o el default. Como los requesters del kanban son nombres de persona (no departamentos), las cards caen al default (`Transformación`). `seed-jira-data` gana flags `--requester-field`/`--requester-default` y la rama de update ahora mergea config para no pisar ediciones de UI.
|
|
- v0.5.1 (2026-06-01) — patch: `handleCreateCard` ahora emite el evento `card.created` (antes solo `board.invalidated`, que no estaba en el filtro del modulo). Con esto la creacion de una card dispara `jiraHandler.create` y sincroniza el alta a Jira, igual que ya ocurria con move (`card.moved`) y chat (`message.created`). El evento se emite tras aplicar assignee/tags para que el issue de Jira los lleve.
|
|
- v0.5.0 (2026-05-27) — minor: merge ramas notifications-realtime + modules con master post-files. Trae notificaciones SSE (tabla `notifications`, migration 015), modulos externos para sincronizacion bidireccional (Jira, etc., tabla `modules`, migration 016), tokens MCP per-user (migration 017). Conserva files attachments del 0128. Renumeradas migrations notif 014/015/016 -> 015/016/017.
|