chore: avance acumulado de sesiones previas (reorg dev/issues + ajustes)
Reorganizacion de dev/issues en subcarpetas (completed/, cpp/, gamedev/, kanban/, trading/, imagegen/, matrix/) y cambios acumulados en cmd/fn/pyrunner, .claude/commands y settings. Trabajo de otro LLM/sesion, commiteado a peticion del usuario para desbloquear el working tree. Excluido logs/ardour_mcp_server.log (ruido).
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: "0058"
|
||||
title: "kanban: sync uses_functions cuando termine WIP en curso"
|
||||
status: pendiente
|
||||
type: docs
|
||||
domain:
|
||||
- kanban
|
||||
- registry-quality
|
||||
scope: multi-app
|
||||
priority: baja
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
# 0058 — kanban: sync uses_functions cuando termine WIP en curso
|
||||
|
||||
## APP Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0058 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | baja |
|
||||
| **Tipo** | docs — `apps/kanban/app.md` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Pre-commit hook v2 ya bloqueara commits que toquen codigo de kanban con drift activo.
|
||||
|
||||
## Contexto
|
||||
|
||||
En sesion 2026-05-07 el audit detecto drift en `apps/kanban/app.md`:
|
||||
|
||||
- Missing (en imports, NO en app.md): `password_hash_go_infra`, `password_verify_go_infra`, `session_create_go_infra`, `session_cleanup_go_infra`, `http_session_cookie_middleware_go_infra` (5 funciones, todas relacionadas con auth).
|
||||
- Unused (en app.md, NO en imports detectados): `spa_handler_go_infra`, `sqlite_open_go_infra`, `http_cors_middleware_go_infra` (3 funciones).
|
||||
|
||||
La sincronizacion se aplazo porque el usuario tiene WIP activo en kanban (auth.go, frontend/src/Root.tsx, LoginPage.tsx, etc. — featurea de auth en construccion).
|
||||
|
||||
## Objetivo
|
||||
|
||||
Cuando el WIP de kanban termine y este commiteado:
|
||||
|
||||
1. Confirmar imports reales con `grep '"fn-registry/' apps/kanban/*.go`.
|
||||
2. Sincronizar `app.md` `uses_functions` con la realidad.
|
||||
3. Verificar `fn doctor uses-functions` reporta kanban limpio.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
### Archivos afectados
|
||||
|
||||
- `apps/kanban/app.md` — actualizar array `uses_functions`.
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1 — esperar WIP commiteado
|
||||
1.1 Confirmar `git -C apps/kanban status` limpio.
|
||||
|
||||
### Fase 2 — sincronizar
|
||||
2.1 Listar imports reales: `grep -rh '"fn-registry/' apps/kanban/*.go | sort -u`.
|
||||
2.2 Cruzar con `uses_functions` actual de `app.md`.
|
||||
2.3 Anadir 5 missing si siguen siendo missing.
|
||||
2.4 Quitar 3 unused si siguen sin usarse (cuidado: el WIP nuevo puede haberlas activado o desactivado otras).
|
||||
|
||||
### Fase 3 — verificar
|
||||
3.1 `fn index`.
|
||||
3.2 `fn doctor uses-functions` debe reportar `kanban_go_tools` sin drift.
|
||||
|
||||
## Riesgos
|
||||
|
||||
- El WIP cambia el conjunto de drift original. Re-correr audit antes de aplicar cambios — no asumir que las 5+3 detectadas el 2026-05-07 son aun validas.
|
||||
|
||||
## Decisiones de diseno
|
||||
|
||||
- Issue trivial pero se documenta para no olvidar (el pre-commit hook ya lo bloquearia, asi que no es urgente — solo corrige fricciones de UX).
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
id: "0063"
|
||||
title: "kanban: sistema de stickers (emojis) sobre cards"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
---
|
||||
# 0063 — kanban: sistema de stickers (emojis) sobre cards
|
||||
|
||||
## APP Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0063 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | media |
|
||||
| **Tipo** | feature — `apps/kanban/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Ninguna. Aplica TBD obligatorio (`.claude/rules/apps_tbd.md`): trabajar en `issue/0063-stickers`, merge `--no-ff` a master.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Permitir que el usuario "decore" visualmente las cards del tablero pegandoles emojis (stickers) con transparencia. Util para marcar estados visualmente sin abusar de tags ni colores: 🔥 (urgente), ✅ (revisado), ⚠️ (cuidado), 🚀 (lanzado), ❤️ (importante), 💀 (bloqueado), etc.
|
||||
|
||||
## Contexto
|
||||
|
||||
`apps/kanban/` es una app Go que embebe un frontend React + Mantine v9 + dnd-kit. El tablero esta en `frontend/src/App.tsx` y cada card se renderiza en `frontend/src/components/KanbanCard.tsx`. Las cards ya tienen color (`color`), tags, asignado, bloqueo, historial y descripcion. Faltan **stickers** como capa decorativa libre, separada del modelo de tags.
|
||||
|
||||
UX deseada:
|
||||
|
||||
1. Usuario hace clic en boton "Stickers" de la toolbar del board (`App.tsx:920`).
|
||||
2. Aparece picker con lista de emojis predefinidos.
|
||||
3. Usuario selecciona uno → entra en "modo pegar sticker" (cursor cambia, indicador visible).
|
||||
4. Hace clic sobre cualquier card → el emoji se pinta encima de la card con opacidad (~0.6).
|
||||
5. ESC sale del modo. Click derecho sobre un sticker existente lo borra.
|
||||
6. Stickers persisten en BD para que sobrevivan recargas y todos los usuarios los vean.
|
||||
|
||||
## Decisiones de diseno (a confirmar antes de implementar)
|
||||
|
||||
| Decision | Recomendacion default | Alternativas |
|
||||
|----------|-----------------------|--------------|
|
||||
| Persistencia | BD (campo `stickers JSON` en cards) | localStorage por usuario |
|
||||
| Cantidad por card | Multiples apilados | Uno solo |
|
||||
| Posicion | Arrastrable dentro de la card | Esquina/centrado fijo |
|
||||
| Tamaño / opacidad | Fijos: 48px, 0.6 | Configurables por sticker |
|
||||
| Fuente emojis | Lista hardcoded ~20 | `emoji-mart`, picker nativo |
|
||||
| UX modo activo | Cursor especial + ESC sale | Toggle persistente |
|
||||
| Borrado | Click derecho sobre sticker | Modo "quitar sticker" |
|
||||
|
||||
Confirmar con usuario antes de Fase 1. Si elige defaults, proceder.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
### Archivos afectados
|
||||
|
||||
**Backend Go (`apps/kanban/`):**
|
||||
- `migrations/00X_add_stickers.sql` (NEW) — `ALTER TABLE cards ADD COLUMN stickers TEXT NOT NULL DEFAULT '[]'`.
|
||||
- `db.go` — leer/escribir columna `stickers` en `Card`.
|
||||
- `handlers.go` — endpoint `PATCH /api/cards/:id/stickers` (body: `{"stickers":[{emoji,x,y}]}`). Reusar el patron de `updateCard`.
|
||||
- `tools.go` — exponer mutacion al chat agent si aplica (opcional).
|
||||
|
||||
**Frontend (`apps/kanban/frontend/src/`):**
|
||||
- `types.ts` — añadir `Sticker { emoji: string; x: number; y: number; }` y `Card.stickers: Sticker[]`.
|
||||
- `api.ts` (NEW endpoint helper) — `updateCardStickers(id, stickers)`.
|
||||
- `App.tsx` — boton "Stickers" en toolbar (`:920`), estado `pickerOpen`, `activeSticker` (emoji seleccionado en modo pegar), handler global ESC, handler `onCardSticker(cardId, x, y)`.
|
||||
- `components/KanbanCard.tsx` — render overlay absoluto de `card.stickers` (z-index alto, pointer-events: none salvo en modo borrado), cuando hay `activeSticker` cambiar cursor + capturar onClick para añadir sticker en coords relativas.
|
||||
- `components/StickerPicker.tsx` (NEW) — Popover con grid de emojis predefinidos.
|
||||
|
||||
**Pure / impure split:**
|
||||
- Pure: util `relativeCoords(event, cardEl) → {x,y}` (frontend, en `components/format.ts` o nuevo `stickers.ts`).
|
||||
- Impure: handler de PATCH HTTP, mutacion de BD, broadcast de cambio si hay realtime.
|
||||
|
||||
### Funciones del registry a reutilizar
|
||||
|
||||
Auditar antes de escribir:
|
||||
|
||||
```bash
|
||||
sqlite3 registry.db "SELECT id, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:emoji OR description:emoji OR name:sticker OR description:sticker');"
|
||||
```
|
||||
|
||||
Probable: nada existe. Si `relativeCoords` resulta reusable → delegar a `fn-constructor` y crear `dom_relative_coords_ts_core` o similar (evaluar tras decision UX).
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1 — confirmar diseno
|
||||
1.1 Confirmar con usuario las 7 decisiones de la tabla. Si elige defaults, seguir.
|
||||
1.2 Auditar registry: `fn search "emoji"`, `fn search "sticker"`. Documentar reutilizacion en este issue.
|
||||
|
||||
### Fase 2 — backend
|
||||
2.1 Crear migracion `migrations/00X_add_stickers.sql`.
|
||||
2.2 Tipo Go `Sticker { Emoji string; X, Y float64 }` en `db.go`. `Card.Stickers []Sticker` con custom marshalling JSON ↔ TEXT.
|
||||
2.3 Endpoint `PATCH /api/cards/:id/stickers` en `handlers.go`. Validar payload (emoji no vacio, x/y en [0,1]).
|
||||
2.4 Aplicar migracion al arrancar (mecanismo existente de `apps/kanban/migrations`).
|
||||
|
||||
### Fase 3 — frontend tipos + api
|
||||
3.1 Añadir `Sticker` y `Card.stickers` en `types.ts`.
|
||||
3.2 Añadir `updateCardStickers(id, stickers)` en `api.ts`.
|
||||
|
||||
### Fase 4 — UI: picker + modo activo
|
||||
4.1 Crear `components/StickerPicker.tsx` con lista hardcoded (≈20 emojis: 🔥⭐✅⚠️🚀💀🎯❤️👀💡📌🐛✨🎉🙏🤔😅🚧🟢🔴).
|
||||
4.2 En `App.tsx`: nuevo state `activeSticker: string | null`. Boton "Stickers" con `IconMoodSmile` o similar de `@tabler/icons-react`. Popover con `StickerPicker`. Listener global `keydown` ESC → `setActiveSticker(null)`.
|
||||
4.3 Indicador visual del modo activo (banner o cursor custom).
|
||||
|
||||
### Fase 5 — render + interaccion en cards
|
||||
5.1 En `KanbanCard.tsx`: render overlay absoluto de `card.stickers` (cada uno como `<span style={{position:'absolute', left:x*100+'%', top:y*100+'%', fontSize:48, opacity:.6, pointerEvents: deleteMode ? 'auto' : 'none'}}>{emoji}</span>`).
|
||||
5.2 Cuando `activeSticker !== null`, en `onClick` de la Paper de la card calcular coords relativas (`event.offsetX / cardEl.offsetWidth`, idem Y, clamp [0,1]), pushear nuevo sticker a `card.stickers`, llamar `updateCardStickers`, optimistic update.
|
||||
5.3 Click derecho sobre un sticker → quitar de la lista + persistir.
|
||||
|
||||
### Fase 6 — tests + verificacion
|
||||
6.1 `tools_test.go`: extender (o nuevo `stickers_test.go`) con test de PATCH `/api/cards/:id/stickers` (insertar 2, borrar 1, leer card).
|
||||
6.2 Manual smoke test: boton aparece, picker funciona, sticker se pega, persiste tras F5, click derecho borra, ESC sale del modo.
|
||||
6.3 `fn doctor uses-functions` no debe regresionar para `kanban_go_tools`.
|
||||
|
||||
### Fase 7 — docs y cleanup
|
||||
7.1 Si se reutilizo o creo funcion del registry → declararla en `apps/kanban/app.md` (`uses_functions`).
|
||||
7.2 Una linea en `apps/kanban/app.md` describiendo la feature.
|
||||
7.3 `fn index` y verificar `fn show kanban_go_tools`.
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
```
|
||||
1. Usuario clica "Stickers" en toolbar del board
|
||||
2. Picker abre, usuario clica 🔥
|
||||
3. Cursor pasa a "modo pegar", banner: "Modo sticker: 🔥 — ESC para salir"
|
||||
4. Usuario clica en card "Migrar usuarios"
|
||||
→ 🔥 aparece donde clico, opacidad 0.6, persistido
|
||||
5. Usuario sigue clicando → mas 🔥 se acumulan
|
||||
6. Pulsa ESC → modo off, cursor normal
|
||||
7. F5 → stickers siguen ahi
|
||||
8. Click derecho sobre 🔥 → desaparece
|
||||
```
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **Hit-test confuso**: si los stickers absorben pointer events, rompen drag-and-drop de la card. Mitigacion: `pointerEvents: 'none'` salvo cuando estamos en modo borrado.
|
||||
- **Coords absolutas vs %**: si guardamos px y la card cambia de ancho (resize de columna), el sticker se sale. Guardar como % (`x,y ∈ [0,1]`).
|
||||
- **Coleccion en chat tool agent**: si el chat agent expone tools que hacen `updateCard` con payload completo, debe respetar el campo `stickers` y no sobrescribirlo a null.
|
||||
- **Migracion de BD en prod (laptop + casa)**: la migracion corre en cada arranque idempotente, OK. Pero confirmar antes en prod local del usuario.
|
||||
- **WIP en kanban (issue 0058)**: este issue toca `app.md` igualmente — coordinar para no pisar el sync pendiente de `uses_functions`.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Confirmar las 7 decisiones de diseno con el usuario (Fase 1).
|
||||
- TBD: rama `issue/0063-stickers` desde `master` actualizado.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: "0089"
|
||||
title: "kanban: tiempo maximo por columna con borde rojo"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## 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,58 @@
|
||||
---
|
||||
id: "0090"
|
||||
title: "kanban: Seleccionar Aleatorio en columna con animacion de ruleta"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
id: "0091"
|
||||
title: "kanban: drag-aware dropzones para abrir/cerrar sidebar"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## Problema
|
||||
|
||||
Mover una card desde el board a una columna que vive en el sidebar (location="sidebar") obliga a:
|
||||
|
||||
1. Soltar la card en el board.
|
||||
2. Hacer click en el toggle del navbar (icono "menu") para abrir el sidebar.
|
||||
3. Volver a coger la card y arrastrarla a la columna sidebar.
|
||||
|
||||
Tres pasos manuales para una operacion que conceptualmente es una sola. Es friccion innecesaria — el sidebar deberia abrirse solo si el usuario claramente intenta llegar a el durante un drag.
|
||||
|
||||
## Solucion
|
||||
|
||||
Anadir una banda invisible de **32px** en el borde izquierdo del viewport (entre el header y el bottom). Mientras hay un drag activo, si el puntero entra en esa banda y se queda mas de **400ms** sin moverse fuera, el sidebar se abre solo (`setNavOpen(true)`).
|
||||
|
||||
### Comportamiento
|
||||
|
||||
- **Auto-open**: si `navOpen=false` y hay drag activo y el puntero entra en la banda → timer 400ms → abrir sidebar.
|
||||
- **Auto-close**: **fuera de scope** en este issue. La friccion principal es abrir. Cerrar el sidebar a mano tras el drop es un click; mantenerlo abierto despues del drop es ademas lo que el usuario suele querer (suelta en sidebar, mira lo que tiene parqueado, decide). Se documenta como followup si surge.
|
||||
- **Feedback visual**: mientras el drag esta activo y el puntero esta dentro de la banda, render de un borde/glow `inset 4px 0 0 var(--mantine-color-blue-4)` con pulso suave.
|
||||
|
||||
### Detalles tecnicos
|
||||
|
||||
- La banda es un `<div>` `position: fixed`, `left: 0`, `top: 50px` (alto del header), `bottom: 0`, `width: 32px`, `z-index: 200` (encima del main, debajo de modals que son `z-index: 1000+`).
|
||||
- `pointer-events: none` para no interferir con el drop target real (board). Detectamos `mouseover/out` mediante `document.addEventListener('mousemove')` solo cuando hay drag activo, y comprobamos `clientX < 32`. Esto evita problemas de event-routing con dnd-kit que ya tiene capturado el pointer.
|
||||
- Timer cancelado en cuanto el puntero sale, en cuanto el drag termina, o si el sidebar ya esta abierto.
|
||||
|
||||
### Frontend
|
||||
|
||||
1. `apps/kanban/frontend/src/App.tsx`
|
||||
- Reusar el state `activeCard`/`activeType` ya existente para saber si hay drag activo (`isDragging = activeCard !== null || activeColumnId !== null`).
|
||||
- Anadir `useEffect` que, mientras `isDragging`, escuche `mousemove` global y dispare timers de hover.
|
||||
- Render del overlay debajo del `AppShell` o dentro pero antes de `AppShell.Main`.
|
||||
|
||||
2. `apps/kanban/frontend/src/styles/dropzone.css` (nuevo)
|
||||
- Animacion `@keyframes` simple para el pulso del borde.
|
||||
- Class `.kanban-drag-edge` con la animacion + transition de opacity.
|
||||
|
||||
3. `apps/kanban/frontend/src/main.tsx`
|
||||
- Import del CSS nuevo.
|
||||
|
||||
4. `apps/kanban/frontend/src/components/KanbanColumn.tsx`
|
||||
- Anadir `data-column-id={column.id}` al Paper raiz para selectores estables en e2e.
|
||||
|
||||
### Tests
|
||||
|
||||
- **Playwright e2e**: `apps/kanban/frontend/e2e/sidebar-dropzone.spec.ts` usando funciones del registry (`pw_kanban_login`, `pw_drag_drop`, `pw_wait_predicate`).
|
||||
- Setup: garantizar via API (`page.request`) que existe al menos una columna sidebar y una card en una columna board.
|
||||
- Login → board cargado → assert sidebar collapsed (width 0 o el slot oculto).
|
||||
- Drag de la card hacia x=10 (banda izquierda) con `hoverMs: 600` para que el timer 400ms se cumpla.
|
||||
- Assert que el sidebar se abrio (`AppShell.Navbar` ahora visible con ancho > 0).
|
||||
- Drop directamente en la columna sidebar visible.
|
||||
- Assert via `/api/board` que la card cambio de `column_id` a la columna sidebar.
|
||||
|
||||
## Criterios de aceptacion
|
||||
|
||||
- [ ] Aparece una banda de 32px en el borde izquierdo cuando hay drag activo (con feedback visual sutil al hover).
|
||||
- [ ] Hover en la banda durante >=400ms con drag activo y sidebar cerrado → sidebar se abre.
|
||||
- [ ] Cancelar el hover (mover puntero fuera antes de 400ms) → sidebar sigue cerrado.
|
||||
- [ ] Terminar drag mientras el sidebar esta abierto por la banda → sidebar sigue abierto (no auto-close).
|
||||
- [ ] Drag card de board a columna sidebar funciona end-to-end en Playwright.
|
||||
- [ ] Vitest existentes siguen pasando.
|
||||
|
||||
## Ramas / commits
|
||||
|
||||
- Rama: `issue/0091-kanban-sidebar-drag-zones`
|
||||
- Merge `--no-ff` a master.
|
||||
|
||||
## Followups potenciales
|
||||
|
||||
- Auto-close: simetria total — cuando se drage **desde** una columna sidebar **hacia** el board, hover en la zona central-derecha cierra el sidebar tras 400ms. Cuesta diseñar bien la "zona de cierre" sin colisionar con la columna sidebar misma; postpuesto.
|
||||
- Persistir preferencia de "abrir-on-drag" en localStorage por si algun usuario no la quiere.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: "0092"
|
||||
title: "kanban: archivo automatico para cards en columnas Done con +30 dias"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: "0093"
|
||||
title: "kanban: reporte diario al pulsar numero del dia en el calendario"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: "0094"
|
||||
title: "kanban: bocadillo del agente + PDF descargable en reporte diario"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: []
|
||||
---
|
||||
|
||||
## Problema
|
||||
|
||||
El reporte diario (issue 0093) muestra tablas y graficos pero no resume el dia en lenguaje natural ni permite compartir el listado de tareas con solicitantes externos.
|
||||
|
||||
## Solucion
|
||||
|
||||
### Backend
|
||||
|
||||
Migration 013 anade:
|
||||
- `daily_summaries (date PK, summary, prompt, model, generated_at, generated_by)` para cachear el resumen del agente por dia.
|
||||
- `settings (key PK, value, updated_at, updated_by)` clave/valor con un seed `daily_report_prompt` por defecto.
|
||||
|
||||
Funciones en `daily_summary.go`:
|
||||
- `GetDailySummary(date)` / `UpsertDailySummary(rec)`.
|
||||
- `GetSetting(key)` / `SetSetting(key, value, by)`.
|
||||
- `runClaudePrompt(ctx, prompt)` ejecuta `claude -p --model <m>` con stdin = prompt y devuelve stdout. Reutiliza `claudeBinary()` y `claudeModel()` del chat.
|
||||
- `BuildDailySummaryPrompt(template, report)` interpola la plantilla con el JSON del reporte.
|
||||
- `GenerateDailySummary(ctx, date, tz, actor)` orquesta: report + template + claude + persist.
|
||||
|
||||
Endpoints:
|
||||
- `GET /api/reports/daily/summary?date=YYYY-MM-DD` -> {exists, summary, prompt, model, generated_at, generated_by}.
|
||||
- `POST /api/reports/daily/summary?date=...&tz=...` regenera y persiste.
|
||||
- `GET /api/settings/{key}` -> {key, value}.
|
||||
- `PUT /api/settings/{key}` body {value} -> 204.
|
||||
|
||||
### Frontend
|
||||
|
||||
`DailyReportView` ahora:
|
||||
- Carga el resumen al abrir (`getDailySummary`). Si no existe, muestra placeholder "Aun no hay resumen del dia".
|
||||
- Bocadillo visual: `Paper` azul claro con borde izquierdo gordo + icono `IconSparkles`, texto del resumen, fecha de generacion + modelo.
|
||||
- Boton `IconRefresh` -> `generateDailySummary`. Loader durante la llamada.
|
||||
- Boton `IconSettings` abre modal con el prompt actual (cargado via `getSetting`). Botones Guardar, Cancelar, Restablecer por defecto.
|
||||
- Filtros nuevos en la tabla de tareas hechas: `Select` solicitante + `Select` asignado. La tabla y el contador "N k/total" reflejan el filtro.
|
||||
- Boton "PDF": abre ventana nueva con HTML print-ready (page A4, CSS inline) que incluye:
|
||||
* Cabecera + sub con filtros activos.
|
||||
* 4 KPIs (Hechas filtradas, Lead time avg, Deadlines on-time, Reabiertas).
|
||||
* Resumen del agente como blockquote azul (si existe).
|
||||
* Tabla con links absolutos `${origin}/?card=${id}` para que el solicitante pueda saltar a cada card.
|
||||
* Auto `window.print()` al cargar.
|
||||
|
||||
### Tests
|
||||
|
||||
`e2e/daily-summary-pdf.spec.ts`:
|
||||
- CRUD roundtrip del setting `daily_report_prompt`.
|
||||
- GET summary del dia devuelve estructura coherente (exists, summary).
|
||||
- UI: modal del reporte expone boton PDF, boton Configurar prompt y boton Regenerar.
|
||||
|
||||
No invocamos `claude -p` real en CI (depende del binario externo). El generador se prueba manualmente.
|
||||
|
||||
## Criterios de aceptacion
|
||||
|
||||
- [ ] Migration 013 crea ambas tablas con seed del prompt por defecto.
|
||||
- [ ] Bocadillo del agente arriba de "Tareas hechas".
|
||||
- [ ] Boton Regenerar invoca al agente, persiste y actualiza la UI.
|
||||
- [ ] Modal de configuracion permite editar y guardar el prompt; reset.
|
||||
- [ ] Filtros solicitante/asignado actualizan tabla y PDF.
|
||||
- [ ] PDF con tabla, KPIs y enlaces internos a cada card.
|
||||
|
||||
## Rama / commits
|
||||
|
||||
- Rama: `issue/0094-kanban-daily-summary-pdf`
|
||||
- Merge `--no-ff` a master.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: "0178"
|
||||
title: "kanban: requester input vacío + navegación con teclado"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- kanban
|
||||
scope: multi-app
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: []
|
||||
created: 2026-05-17
|
||||
updated: 2026-05-17
|
||||
tags: [ausente-ready]
|
||||
---
|
||||
|
||||
## Problema
|
||||
|
||||
Al crear una nueva card en el kanban (`Nueva tarjeta` modal), el campo "Solicitante" se pre-rellena con el `display_name` del usuario logueado. Esto fuerza al usuario a borrar el contenido si quiere otro solicitante.
|
||||
|
||||
Además, al escribir en el Autocomplete:
|
||||
- La tecla `Enter` dispara el submit del formulario (línea 51-56 de `CardForm.tsx`) en lugar de seleccionar la sugerencia resaltada del dropdown.
|
||||
- Las flechas ↑↓ no son navegables porque el Autocomplete de Mantine sí las soporta nativamente, pero el `onKeyDown` con submit-on-Enter las invalida (al pulsar Enter para confirmar la selección, se cierra el form sin haber seleccionado).
|
||||
|
||||
## Solución
|
||||
|
||||
### Frontend
|
||||
|
||||
1. `apps/kanban/frontend/src/App.tsx:548`
|
||||
```ts
|
||||
initial={{ requester: auth.user?.display_name || auth.user?.username || "" }}
|
||||
```
|
||||
→ cambiar a:
|
||||
```ts
|
||||
initial={{ requester: "" }}
|
||||
```
|
||||
(Quitar pre-fill. Si más adelante se quiere "último solicitante usado", se hace en otro issue.)
|
||||
|
||||
2. `apps/kanban/frontend/src/components/CardForm.tsx`
|
||||
- Cambiar el handler `enterSubmit` aplicado al `Autocomplete` de "Solicitante". Para Enter en el requester:
|
||||
- Si dropdown abierto → dejar que Mantine maneje la selección (no `e.preventDefault()`, no `submit()`).
|
||||
- Si dropdown cerrado → no hacer nada (no `submit()`).
|
||||
- El submit se hace solo con el botón "Crear" (o Ctrl+Enter desde el textarea de descripción, que ya existe).
|
||||
- Añadir `data-field="requester"` al Autocomplete para selectores estables en e2e.
|
||||
|
||||
### Tests
|
||||
|
||||
- **vitest componente**: `apps/kanban/frontend/src/components/CardForm.test.tsx`
|
||||
- Render con `requesterOptions=["Alice","Anna","Bob"]`, sin `initial.requester`.
|
||||
- Assert input vacío.
|
||||
- Type "An" → dropdown muestra Alice + Anna (filtro).
|
||||
- Press ArrowDown → highlight Alice, press ArrowDown → highlight Anna, press Enter → input vale "Anna" y `onSubmit` NO se llamó.
|
||||
- Press Enter en input cuando dropdown cerrado → `onSubmit` NO se llamó.
|
||||
- Click en botón "Crear" → `onSubmit` llamado con `requester: "Anna"`.
|
||||
|
||||
- **Playwright e2e**: `apps/kanban/frontend/e2e/requester-input.spec.ts` usando funciones del registry (`pw_launch_browser`, `pw_kanban_login`, `pw_keyboard_sequence`, `pw_assert_class`).
|
||||
- Login → click "+ Nueva tarjeta" en cualquier columna.
|
||||
- Assert input requester vacío.
|
||||
- Type "Enma", esperar 200ms (debounce dropdown).
|
||||
- ArrowDown + Enter → assert modal sigue visible y input contiene un valor.
|
||||
- Click "Crear" → modal se cierra y card aparece en columna.
|
||||
|
||||
## Criterios de aceptación
|
||||
|
||||
- [ ] Input "Solicitante" entra vacío al abrir "Nueva tarjeta".
|
||||
- [ ] Enter dentro del Autocomplete NO cierra el form.
|
||||
- [ ] ↑↓ navegan el dropdown y Enter selecciona la entrada resaltada.
|
||||
- [ ] Botón "Crear" sigue funcionando como único submit del form.
|
||||
- [ ] Tests vitest + e2e Playwright pasan.
|
||||
|
||||
## Ramas / commits
|
||||
|
||||
- Rama: `issue/0088-kanban-requester-empty-nav`
|
||||
- Merge `--no-ff` a master.
|
||||
Reference in New Issue
Block a user