From 489d2bbef67d2f2d11d239b67dc9a45c8b8d3bf0 Mon Sep 17 00:00:00 2001 From: egutierrez Date: Wed, 27 May 2026 10:52:06 +0200 Subject: [PATCH] chore: bump kanban 0.1.0 -> 0.2.0 + e2e smoke (issue 0128) - app.md: descripcion, e2e_checks smoke_files, doc Archivos, capability growth log - .gitignore: uploads/ - e2e/files_smoke.sh: build, login, upload PNG, list, serve, delete --- .gitignore | 3 ++ app.md | 16 +++++++-- e2e/files_smoke.sh | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100755 e2e/files_smoke.sh diff --git a/.gitignore b/.gitignore index b2fbf5e..0a83ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ frontend/tsconfig.tsbuildinfo # Local files local_files/ +# Card file attachments (issue 0128) — binarios en disco; metadata en card_files +uploads/ + # Logs *.log frontend/test-results/ diff --git a/app.md b/app.md index 39f034e..8a113d6 100644 --- a/app.md +++ b/app.md @@ -2,8 +2,8 @@ name: kanban lang: go domain: tools -version: 0.1.0 -description: "Kanban board con persistencia SQLite, drag-and-drop entre columnas (dnd-kit) y tracking del tiempo que cada tarjeta pasa en cada columna. Frontend Vite + React + Mantine v9 embebido en el binario Go." +version: 0.2.0 +description: "Kanban board con persistencia SQLite, drag-and-drop entre columnas (dnd-kit), tracking del tiempo por columna y adjuntos de archivos por card (drag&drop en descripcion y chat). Frontend Vite + React + Mantine v9 embebido en el binario Go." tags: [service, kanban, web, dnd-kit, mantine, sqlite, time-tracking] uses_functions: - random_hex_id_go_core @@ -81,6 +81,10 @@ e2e_checks: 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 @@ -143,7 +147,12 @@ Single-binary: backend Go con frontend Vite embebido. SQLite local con tres tabl - **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 (proximamente):** blobs persistidos en SQLite (`card_attachments` con `BLOB`), no en filesystem. +- **Archivos** (`CardFilesPanel`): adjuntos por card almacenados en `apps/kanban/uploads//__` (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 `![name](url)` (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 `![alt](url)` -> `` thumb 220px y `[name](url)` -> ``. 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 @@ -182,3 +191,4 @@ Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`: - `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. diff --git a/e2e/files_smoke.sh b/e2e/files_smoke.sh new file mode 100755 index 0000000..21cfe8e --- /dev/null +++ b/e2e/files_smoke.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Issue 0128 — smoke test of card file attachments. +# Builds kanban (assumes ./kanban present or builds it), boots on ephemeral port, +# exercises POST upload, GET list, GET serve, DELETE, GET list-after-delete. +# Exits 0 on success, non-zero on any failure. + +set -euo pipefail + +PORT=${PORT:-18095} +BASE="http://127.0.0.1:${PORT}" +DB=$(mktemp /tmp/kanban_files_smoke.XXXXXX.db) +COOKIE=$(mktemp /tmp/kanban_files_smoke.cookie.XXXXXX) +UPLOAD_DIR=$(dirname "$DB")/uploads +PNG=$(mktemp /tmp/kanban_files_smoke.XXXXXX.png) +PID_FILE=$(mktemp /tmp/kanban_files_smoke.pid.XXXXXX) + +cleanup() { + if [ -s "$PID_FILE" ]; then + kill "$(cat "$PID_FILE")" 2>/dev/null || true + fi + rm -f "$DB" "$DB-shm" "$DB-wal" "$COOKIE" "$PNG" "$PID_FILE" + rm -rf "$UPLOAD_DIR" +} +trap cleanup EXIT + +# Build if missing. +if [ ! -x ./kanban ]; then + echo "[smoke] building kanban binary..." + (cd backend && CGO_ENABLED=1 go build -tags fts5 -o ../kanban .) +fi + +# Boot. +./kanban --port "$PORT" --db "$DB" --initial-admin admin:adminpw \ + > /tmp/kanban_files_smoke.log 2>&1 & +echo $! > "$PID_FILE" + +# Wait for /api/board. +for _ in $(seq 1 30); do + if curl -sf -o /dev/null "$BASE/api/board"; then break; fi + sleep 0.2 +done + +# Login. +curl -sf -c "$COOKIE" -X POST "$BASE/api/auth/login" \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"adminpw"}' > /dev/null + +# Column + card. +COL=$(curl -sf -b "$COOKIE" -X POST "$BASE/api/columns" \ + -H 'Content-Type: application/json' \ + -d '{"name":"To Do"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') +CARD=$(curl -sf -b "$COOKIE" -X POST "$BASE/api/cards" \ + -H 'Content-Type: application/json' \ + -d "{\"column_id\":\"$COL\",\"title\":\"smoke\",\"requester\":\"r\"}" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') + +# Minimal PNG. +printf '\x89PNG\r\n\x1a\n' > "$PNG" + +# Upload. +UP=$(curl -sf -b "$COOKIE" -X POST "$BASE/api/cards/$CARD/files" \ + -F "file=@$PNG;type=image/png") +FID=$(echo "$UP" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') +[ -n "$FID" ] || { echo "[smoke] upload missing id"; exit 1; } + +# List active. +N=$(curl -sf -b "$COOKIE" "$BASE/api/cards/$CARD/files" | python3 -c 'import sys,json;print(len(json.load(sys.stdin)))') +[ "$N" = "1" ] || { echo "[smoke] expected 1 file, got $N"; exit 1; } + +# Serve. +CT=$(curl -sf -b "$COOKIE" -I "$BASE/api/files/$FID" | awk '/^[Cc]ontent-[Tt]ype/ {print $2}' | tr -d '\r\n') +echo "$CT" | grep -q image/png || { echo "[smoke] wrong content-type: $CT"; exit 1; } + +# Delete. +HTTP=$(curl -sb "$COOKIE" -X DELETE -o /dev/null -w "%{http_code}" "$BASE/api/files/$FID") +[ "$HTTP" = "204" ] || { echo "[smoke] expected 204 on delete, got $HTTP"; exit 1; } + +# List after delete. +N=$(curl -sf -b "$COOKIE" "$BASE/api/cards/$CARD/files" | python3 -c 'import sys,json;print(len(json.load(sys.stdin)))') +[ "$N" = "0" ] || { echo "[smoke] expected 0 after delete, got $N"; exit 1; } + +echo "[smoke] OK"