From 60af81c104f7b55ce25712d899ed1f5b0bf25f6c Mon Sep 17 00:00:00 2001 From: egutierrez Date: Thu, 14 May 2026 17:57:44 +0200 Subject: [PATCH] docs(issues): kanban 0089-0093 reportes diarios + perf + archive Archivos de issue para el trabajo de kanban de las ultimas iteraciones: - 0089: tiempo maximo por columna con borde rojo (incluye followup popover con seleccion de unidad min/h/d/sem/mes). - 0090: seleccion aleatoria por columna con animacion de ruleta. Ya con fix de no mostrar en columnas Done. - 0092: archivo automatico para cards en columnas Done con +30 dias. - 0093: reporte diario al pulsar el numero del dia en el calendario. Los issues 0088 y 0091 ya estaban registrados. --- dev/issues/0089-kanban-column-max-time.md | 51 +++++++++++++++ dev/issues/0090-kanban-column-random-pick.md | 50 +++++++++++++++ dev/issues/0092-kanban-done-archive.md | 65 +++++++++++++++++++ dev/issues/0093-kanban-daily-report.md | 67 ++++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 dev/issues/0089-kanban-column-max-time.md create mode 100644 dev/issues/0090-kanban-column-random-pick.md create mode 100644 dev/issues/0092-kanban-done-archive.md create mode 100644 dev/issues/0093-kanban-daily-report.md diff --git a/dev/issues/0089-kanban-column-max-time.md b/dev/issues/0089-kanban-column-max-time.md new file mode 100644 index 00000000..002141a0 --- /dev/null +++ b/dev/issues/0089-kanban-column-max-time.md @@ -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. diff --git a/dev/issues/0090-kanban-column-random-pick.md b/dev/issues/0090-kanban-column-random-pick.md new file mode 100644 index 00000000..975dbe7d --- /dev/null +++ b/dev/issues/0090-kanban-column-random-pick.md @@ -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. diff --git a/dev/issues/0092-kanban-done-archive.md b/dev/issues/0092-kanban-done-archive.md new file mode 100644 index 00000000..88990027 --- /dev/null +++ b/dev/issues/0092-kanban-done-archive.md @@ -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. diff --git a/dev/issues/0093-kanban-daily-report.md b/dev/issues/0093-kanban-daily-report.md new file mode 100644 index 00000000..2caee90b --- /dev/null +++ b/dev/issues/0093-kanban-daily-report.md @@ -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 `` 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.