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
This commit is contained in:
@@ -16,6 +16,9 @@ frontend/tsconfig.tsbuildinfo
|
|||||||
# Local files
|
# Local files
|
||||||
local_files/
|
local_files/
|
||||||
|
|
||||||
|
# Card file attachments (issue 0128) — binarios en disco; metadata en card_files
|
||||||
|
uploads/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
frontend/test-results/
|
frontend/test-results/
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
name: kanban
|
name: kanban
|
||||||
lang: go
|
lang: go
|
||||||
domain: tools
|
domain: tools
|
||||||
version: 0.1.0
|
version: 0.2.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."
|
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]
|
tags: [service, kanban, web, dnd-kit, mantine, sqlite, time-tracking]
|
||||||
uses_functions:
|
uses_functions:
|
||||||
- random_hex_id_go_core
|
- random_hex_id_go_core
|
||||||
@@ -81,6 +81,10 @@ e2e_checks:
|
|||||||
cmd: "go test -tags fts5 -count=1 ./..."
|
cmd: "go test -tags fts5 -count=1 ./..."
|
||||||
timeout_s: 120
|
timeout_s: 120
|
||||||
expect_exit: 0
|
expect_exit: 0
|
||||||
|
- id: smoke_files
|
||||||
|
cmd: "bash e2e/files_smoke.sh"
|
||||||
|
timeout_s: 30
|
||||||
|
expect_exit: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
## Arquitectura
|
## 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"`).
|
- **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)".
|
- **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.
|
- **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/<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
|
### Build
|
||||||
|
|
||||||
@@ -182,3 +191,4 @@ Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
|||||||
- `patch`: bugfix sin cambio observable.
|
- `patch`: bugfix sin cambio observable.
|
||||||
|
|
||||||
- v0.1.0 (2026-05-18) — baseline.
|
- 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.
|
||||||
|
|||||||
Executable
+82
@@ -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"
|
||||||
Reference in New Issue
Block a user