Merge quick/kanban-issue-docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 17:58:02 +02:00
4 changed files with 233 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
---
id: "0089"
title: "kanban: tiempo maximo por columna con borde rojo"
status: open
created_at: 2026-05-14
priority: medium
app: kanban
---
## Problema
No hay forma de marcar visualmente cards que llevan demasiado tiempo en una columna. Cards estancadas se diluyen entre el resto. El campo `card.time_in_column_ms` ya existe en `/api/board`, falta el limite por columna y el borde visual.
## Solucion
### Migration (preserva datos)
`apps/kanban/backend/migrations/011_column_max_time.sql``ALTER TABLE columns ADD COLUMN max_time_minutes INTEGER NOT NULL DEFAULT 0`. 0 = sin limite.
### Backend
- `Column` struct: `MaxTimeMinutes int json:"max_time_minutes"`.
- `ColumnPatch` + `UpdateColumn`: soporte para el nuevo campo, clamp a >= 0.
- `handleUpdateColumn` (PATCH `/api/columns/:id`): acepta `max_time_minutes` opcional.
- `ListColumns`: incluye el nuevo campo en el SELECT.
### Frontend
- `Column` TS interface + `UpdateColumnInput`: nuevo campo.
- `KanbanColumn` menu contextual: "Tiempo maximo" via `window.prompt` (idempotente, sin nuevo modal). Muestra valor actual en la entrada del menu si > 0.
- `KanbanColumn` -> `KanbanCard`: prop nueva `columnOverdue` calculada como `!is_done && max_time_minutes > 0 && time_in_column_ms > max_time_minutes * 60_000`.
- `KanbanCard`: cuando `columnOverdue` y NO highlighted ni locked, pinta `border-color: var(--mantine-color-red-6)` + `border-width: 2` + halo rojo. data-attribute `data-column-overdue` para tests.
- Columnas con `is_done=true` nunca disparan overdue (regla del usuario).
### Tests
- Backend Go: `TestColumnMaxTimeMinutes_Defaults`, `TestColumnMaxTimeMinutes_Update` (default 0, clamp negativo, no toca otros campos).
- Frontend: testing visual del borde rojo via Playwright queda fuera de scope automatizado (requiere control de reloj o cards reales con > N min). Cubierto con verificacion manual al finalizar.
## Criterios de aceptacion
- [ ] Migration aplica sin perder datos.
- [ ] Menu contextual "Tiempo maximo" disponible en cada columna.
- [ ] Borde rojo aparece en cards `time_in_column_ms > max_time_minutes * 60s` cuando `is_done=false`.
- [ ] Columnas Done nunca muestran borde rojo aunque tengan limite configurado.
- [ ] Tests Go pasan (2).
## Rama / commits
- Rama: `issue/0089-kanban-column-max-time`
- Merge `--no-ff` a master.
@@ -0,0 +1,50 @@
---
id: "0090"
title: "kanban: Seleccionar Aleatorio en columna con animacion de ruleta"
status: open
created_at: 2026-05-14
priority: medium
app: kanban
---
## Problema
Cuando hay muchas cards en una columna, elegir cual abordar "al azar" es ruido cognitivo. Necesitamos boton "Seleccionar Aleatorio" que respete los filtros activos y excluya cards bloqueadas.
## Solucion
### Frontend (puro, sin backend)
- `KanbanColumn` menu contextual: nueva entrada "Seleccionar Aleatorio" (`IconDice5`), `data-test="column-random-pick"`. Deshabilitada cuando `cards.filter(c => !c.locked).length === 0`.
- Prop `onPickRandom(columnId)` propagada desde `KanbanColumn` a `App.tsx`.
- `App.tsx` handler `handlePickRandom`:
- Lee cards del column id via `cardsByColumn` (ya respeta los filtros).
- Excluye `card.locked === true`.
- Si `length === 0`: notificacion warning.
- Si `length === 1`: scroll + flash verde directo.
- Si `length > 1`: ruleta animada.
- Ruleta:
- Seleccion criptografica con `crypto.getRandomValues`.
- Cycle: 2 vueltas completas + offset hasta la ganadora.
- Decay temporal cubic: 50ms inicial -> 220ms final (efecto desacelerar).
- CSS class `.kanban-roulette-active` aplicada a la card en curso, `scrollIntoView` automatico para seguirla.
- Al final: `.kanban-roulette-winner` con pulso verde (~1.6s).
- CSS en `src/styles/roulette.css`, importado desde `main.tsx`.
### Tests
- Verificacion manual: probar en columna con varias cards, con y sin filtros activos, con cards bloqueadas. Comportamiento esperado: solo unlocked-and-filtered participan.
- E2E Playwright queda fuera de scope automatizado por timing visual y por necesitar cards reales en una columna concreta.
## Criterios de aceptacion
- [ ] Menu contextual "Seleccionar Aleatorio" en cada columna.
- [ ] Cards bloqueadas excluidas siempre.
- [ ] Si hay filtro activo, solo cards visibles participan.
- [ ] Animacion ruleta visible, no se queda colgada.
- [ ] Ganadora final con flash verde + scroll a su posicion.
## Rama / commits
- Rama: `issue/0090-kanban-column-random-pick`
- Merge `--no-ff` a master.
+65
View File
@@ -0,0 +1,65 @@
---
id: "0092"
title: "kanban: archivo automatico para cards en columnas Done con +30 dias"
status: open
created_at: 2026-05-14
priority: medium
app: kanban
---
## Problema
Las columnas Done acumulan decenas de cards completadas que ya no participan en el trabajo activo. Lastran el board, gastan render (incluso con el split memoizado por perf) y obligan a scroll horizontal hasta el infinito sin aportar valor diario.
## Solucion
Archive `archived_at` por card (independiente de papelera `deleted_at`). Card archivada sale del board pero sigue en BD. Cajon "Hecho (archivo)" en el sidebar bajo Papelera con boton para sacar una card de vuelta a su columna Done.
### Migration
`apps/kanban/backend/migrations/012_card_archived.sql`:
```sql
ALTER TABLE cards ADD COLUMN archived_at TEXT;
```
`NULL` = card activa. ISO timestamp = card archivada.
### Backend
- `Card.ArchivedAt *string` json `archived_at`.
- `ListCardsWithTime` filtra `archived_at IS NULL`.
- `ArchiveCard(id)`, `UnarchiveCard(id)`, `ListArchivedCards()`.
- `AutoArchiveDoneOlderThan(d)`: UPDATE cards SET archived_at=now WHERE archived_at IS NULL AND deleted_at IS NULL AND column_id IN (SELECT id FROM columns WHERE is_done=1) AND id IN (SELECT card_id FROM card_column_history WHERE exited_at IS NULL AND entered_at < cutoff).
- `maybeAutoArchive(db)` con throttle `archiveSweepEvery = 30m` y `archiveAfter = 30d`. Disparado lazy desde `handleGetBoard`.
- Rutas:
- `GET /api/archive`
- `POST /api/cards/{id}/archive`
- `POST /api/cards/{id}/unarchive`
### Frontend
- Tipo `Card.archived_at: string | null`.
- `api.listArchive()` / `api.archiveCard(id)` / `api.unarchiveCard(id)`.
- `App` state `archive`, `archiveOpen`, `reloadArchive`, `handleArchiveCard`, `handleUnarchiveCard`. `reloadArchive` se llama tras login + tras archivar/desarchivar.
- UI: cajon "Hecho (archivo)" bajo "Papelera" en el sidebar. Toggle similar (`Badge` con conteo, `IconChevronRight/Down`). Solo boton "Restaurar" (`IconArrowBackUp`). No hay "purgar permanentemente" desde el archivo — solo borrado via card normal en su columna.
- `KanbanCard`: nuevo `Menu.Item` "Archivar" (`IconArchive` teal) condicionado a `isDone && onArchive` antes del divisor + Borrar.
### Tests
- Playwright `e2e/archive.spec.ts`: archivar manual via menu de card en columna done, asserta desaparece del board, abre cajon "Hecho", des-archiva, asserta vuelve al board.
- Auto-archive 30d sin test e2e (timing real no es practico). Cubierto por audit manual del flag `archiveAfter` y la query SQL.
## Criterios de aceptacion
- [ ] Cards con +30 dias en columna Done se auto-archivan tras la primera lectura del board (max 30m).
- [ ] Cajon "Hecho (archivo)" visible bajo Papelera. Muestra conteo.
- [ ] Boton "Restaurar" devuelve la card a su columna sin reset de historial.
- [ ] Menu de card en columna Done muestra "Archivar" para archivado manual.
- [ ] Cards archivadas NO aparecen en `/api/board`.
- [ ] E2E `archive.spec.ts` pasa.
## Rama / commits
- Rama: `issue/0092-kanban-done-archive`
- Merge `--no-ff` a master.
+67
View File
@@ -0,0 +1,67 @@
---
id: "0093"
title: "kanban: reporte diario al pulsar numero del dia en el calendario"
status: open
created_at: 2026-05-14
priority: medium
app: kanban
---
## Problema
El calendario muestra conteos de creadas/hechas/deadlines por dia pero no permite profundizar. Falta un reporte tipo "retro diaria" con rankings, lista de tareas hechas con enlaces, y metricas operativas (lead time, reabiertas, estancadas).
## Solucion
### Backend
Endpoint `GET /api/reports/daily?date=YYYY-MM-DD&tz=Europe/Madrid` con agregaciones sobre `cards`, `card_events`, `card_column_history`, `card_lock_history`.
Estructura del reporte:
- `kpis`: done, created, moves, blocked_ms, deadlines met/missed, reopened, archived_auto, archived_manual.
- `top_assignees_done`, `top_assignees_created`, `top_requesters_added`, `top_requesters_done` (top 5).
- `done_cards`: lista completa de hechas con `id`, `seq_num`, titulo, solicitante, asignado (id+nombre), tags, columna, completed_at, lead_time_ms.
- `reopened_cards`: cards que el dia X entraron a una columna no-done viniendo de una previa done. Incluye actor.
- `stale_cards`: 3 buckets (7-13d, 14-29d, 30+d) por columnas activas (no done, no archived).
- `lead_time`: avg/p50/p95/samples de las hechas del dia.
- `hourly_moves`: array de 24 con conteo de movimientos por hora local.
- `deadlines`: contadores + lista de vencidas con `late_ms`.
- `tags_done`: top 10 tags trabajadas hoy.
- `archived_today`: total archivadas en el dia.
Funcion: `db.DailyReportFor(date, tz)` en `backend/reports.go`. Day range = [t, t+24h) en TZ del cliente, convertido a UTC para consultar columnas TEXT ISO.
### Frontend
Componente `DailyReportView` (`components/DailyReport.tsx`) que recibe `date` y `onJumpToCard`. Layout:
- Header: titulo + fecha formateada en es-ES.
- 6 KPI cards (`SimpleGrid` cols 2/4/6).
- 4 rankings con avatares (`SimpleGrid` cols 1/2/4).
- Tabla de tareas hechas (scrollable, click en titulo -> `onJumpToCard` -> cierra modal -> highlight + scroll al tablero).
- `BarChart` (Mantine Charts) con movimientos por hora.
- Tags trabajadas en chips.
- Bloque reabiertas (solo si hay).
- Bloque deadlines (solo si hay actividad).
- 3 columnas de estancadas con click-to-jump.
Apertura: `CalendarView` recibe `onOpenDailyReport(date)` y envuelve el numero del dia en un `UnstyledButton` (`data-test="calendar-day-YYYY-MM-DD"`). `App` abre un `modals.open` con `size="90%"` y `<DailyReportView />` dentro.
### Tests
Playwright `e2e/daily-report.spec.ts`:
- Endpoint devuelve estructura esperada (kpis, done_cards, hourly_moves[24], stale_cards.d7/d14/d30).
- Click en numero del dia del calendario abre el modal con titulo "Reporte diario" y textos "Hechas"/"Movimientos" visibles.
## Criterios de aceptacion
- [ ] GET `/api/reports/daily?date=...&tz=...` responde JSON estructurado.
- [ ] Click en numero del dia en el calendario abre modal full-width con KPIs.
- [ ] Tabla de hechas con click-to-jump funcional.
- [ ] Reabiertas detectadas correctamente (card que viene de columna `is_done=1` previa).
- [ ] Estancadas listadas por buckets 7/14/30d.
- [ ] Tests Playwright pasan.
## Rama / commits
- Rama: `issue/0093-kanban-daily-report`
- Merge `--no-ff` a master.