63 Commits

Author SHA1 Message Date
egutierrez 466a055f72 chore: auto-commit (1 archivos)
- app.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 15:49:55 +02:00
egutierrez a934897099 merge: fill required Área Solicitante on Jira create (v0.5.2) 2026-06-01 15:41:19 +02:00
egutierrez 0687b65ea2 fix(jira): fill required 'Área Solicitante' on Epic create
Project DATA's Epic and Mejora issue types mark customfield_10158
('Área Solicitante', a single-select) as required on the create screen.
The create payload omitted it, so enabling card.created sync produced
HTTP 400 'Solicitante is required'.

Add RequesterField/RequesterMap/RequesterDefault to jiraConfig. create()
and update() now inject the field as a {value:<option>} single-select,
resolved from the card requester via the map (case-insensitive) or the
default. Kanban requesters are person names, not departments, so cards
fall through to requester_default ('Transformación' for our setup).

seed-jira-data gains --requester-field (default customfield_10158) and
--requester-default (default Transformación); the existing-module branch
now merges config so operator UI edits (e.g. a requester_map) survive a
re-seed. Validated against Jira: Epic create with the field succeeds 201
(reporter auto-defaults to the token owner).
2026-06-01 15:41:19 +02:00
egutierrez 87e8f62544 merge: jira sync on card creation (v0.5.1) 2026-06-01 15:25:26 +02:00
egutierrez 0d8ec1e8e7 fix(jira): emit card.created so card creation syncs to Jira
handleCreateCard only published board.invalidated, which is not in the
module event filter, so the dispatcher dropped it and jiraHandler.create
never ran. Newly created cards therefore never produced a Jira issue,
unlike moves (card.moved) and chat (message.created) which already synced.

Emit card.created after assignee/tags are applied so the synced issue
carries them. board.invalidated is kept for the SPA refetch path. No loop
risk (card.created fires only from the HTTP handler) and no double-create
(board.invalidated stays out of the filter).
2026-06-01 15:25:25 +02:00
egutierrez d4558667f6 feat(jira): menu 'Jira' (rename) + modal con tabs Importar/Comprobar columnas
UI:
- Menu avatar dropdown: 'Importar de Jira' -> 'Jira' (renombrado).
- ImportJiraModal.tsx eliminado. Sustituido por JiraModal.tsx con Mantine Tabs:
  * 'Importar de Jira': UI heredada del modal anterior intacta.
  * 'Comprobar columnas': nueva. Lista cards linked y muestra desincronizadas
    (kanban col vs Jira status actual). Por cada row: kanban col + expected jira
    status + jira status real. Checkbox multi-select + boton 'Sincronizar' que
    empuja Jira al status correcto (kanban gana).

Backend:
- GET /api/jira/check-columns: walk cards.jira_key != ''. Por cada uno GET
  /rest/api/3/issue/{key}?fields=status. Compara status real vs status_map.
  Devuelve {rows[], total, mismatches, in_sync, status_map, reverse_map}.
- POST /api/jira/reconcile-columns {card_ids[], direction:'kanban-wins'}:
  reusa jiraHandler.transitionToStatus para empujar cada issue al status del
  status_map de su columna kanban actual + actualiza cards.jira_last_status.
- Helper listLinkedCardsForCheck en jira_import.go.

Direction='kanban-wins' default. Reverse direction (jira-wins) no soportado
por ahora: mover cards desde el server tiene efectos colaterales (eventos,
notificaciones, timers) que no quiero disparar masivos sin pensar.
2026-05-29 15:18:59 +02:00
egutierrez 9b0b6e516c fix(jira): emitir card.moved al cambiar columna + adaptive indicator polling
Bug: handleMoveCard solo emitia board.invalidated. Dispatcher mapeaba a
update() (PUT summary/description/labels) NUNCA a transition(), asi que
mover una card en kanban no transicionaba su Jira issue de columna. Solo
los labels reflejaban el cambio.

Fix backend (handlers.go):
- handleMoveCard ahora lee column_id antes del MoveCard. Si la card crusa
  columnas (prev != new) publica 'card.moved' antes de 'board.invalidated'.
  El dispatcher reconoce 'card.moved' y ejecuta transition() -> Jira status
  cambia + labels sincronizan.
- Reorder dentro de la misma columna sigue como antes: solo board.invalidated
  para refetch del cliente sin tocar Jira.
- nuevo helper db.lookupCardColumnID(cardID).

UX frontend (JiraSyncIndicator):
- Polling adaptativo: 5s steady, 1s mientras inflight=true. El usuario VE
  el yellow durante el sync.
- Listener de window CustomEvent 'kanban-card-moved' (cardId match) que
  fuerza un refetch inmediato (~150ms) tras drop. App.tsx dispara el evento
  tras api.moveCard resolve. Yellow visible casi instantaneo en lugar de
  esperar al proximo tick steady.
2026-05-29 15:01:51 +02:00
egutierrez c5113f75a5 feat(jira): issue_type=Epic + AssigneeMap + CLI resync-jira-fields
Cambios:
- jiraConfig: nuevo campo AssigneeMap (kanban_user_id -> jira_accountId).
- jiraHandler.create() y update(): aplican fields.assignee={accountId} cuando
  card.AssigneeID esta en el map. NO se borra el assignee de Jira cuando no
  hay mapeo (evita pisar asignaciones manuales).
- resolveJiraAssignee: helper compartido.
- seed-jira-data: cambio issue_type default Tarea Tecnica -> Epic (board 33
  filtra issuetype=Epic). assignee_map inyectada con 3 mapeos confirmados:
    egutierrez (Enmaa)  -> 712020:2cf3b82f-... (Enmanuel Gutierrez Perez)
    amassaguer (alfon)  -> 712020:3f3ca9e1-... (Alfonso Massaguer Gomez)
    ntajuelo   (Nat)    -> 712020:feb5f7c5-... (Natalia Tajuelo Gomez)
- Nueva CLI 'kanban resync-jira-fields' con flags
    --set-issuetype/--set-assignee/--set-labels/--dry-run/--limit/--batch-size/--pause-sec
  Idempotente. PUT /rest/api/3/issue/{key} con los fields del config actual.
  Usado para patchear las 127 issues ya creadas con Tarea Tecnica -> Epic +
  assignee (donde mapea).
- Ejecutado: 127/127 OK, 0 fail. Board 33 ahora muestra 219 issues totales
  (92 Epics previas + 127 nuevas). Sample verificado contra Jira REST API.
2026-05-29 14:52:48 +02:00
egutierrez cd14e81487 feat(jira): kanban backfill-jira CLI con batches + ejecutado backfill 127 cards
Subcomando 'kanban backfill-jira':
- --batch-size N --pause-sec S: procesa N cards entre pausas para no saturar Jira REST.
- --limit N: cap total.
- --column NAME: filtro case-insensitive por columna kanban.
- --dry-run: lista candidatos + counts por columna sin tocar Jira.

Walk: cards con jira_key vacio, no borradas, no archivadas, ORDER BY created_at ASC.
Por cada card: jiraHandler.Handle(card.created event) que crea issue + transition al
status del status_map + labels. Tras success/failure updateCardJiraSync persiste
jira_last_status, jira_last_sync_at, jira_last_error.

Ejecutado contra Jira DATA project: 127 issues creadas (DATA-276..DATA-402), 0 fail.
Distribucion final:
  Done: 85 (HECHO)
  In Progress: 18 (HACIENDO 7 + Bloqueadas 11, las ultimas con label 'blocked')
  IMPLEMENTADO: 14 (PNDNT FEEDBACK)
  To Do: 6 (DEUDA TECNICA)
  CREADO: 4 (IDEAS)
2026-05-29 14:37:56 +02:00
egutierrez c3cc42b350 feat(jira): indicator per-card + import view desde Jira board 33
Backend:
- migration 018: cards.jira_last_status / sync_at / error (estado persistido del ultimo
  sync para render UI sin polling Jira).
- Dispatcher: sync.Map inflight para 'yellow' realtime + persistencia de exito/fallo
  en cards tras cada dispatch attempt.
- GET /api/cards/{id}/jira-sync: devuelve {jira_key, last_status, last_sync_at,
  last_error, inflight, issue_url} para el tooltip del indicador.
- GET /api/jira/issues: lista issues del board 33 con flag already_imported +
  mapped_column_id (reverse status_map). Filtros include_imported, limit.
- POST /api/jira/import: multi-key. Cada issue -> CreateCard + setCardJiraKey +
  seed jira_last_status. Cae en columna mapeada por status, o en fallback_column_id.
  ADF de description extraido a texto plano.

Frontend:
- JiraSyncIndicator: dot gris/amarillo/verde/rojo bajo IconDotsVertical de cada card.
  Mantine HoverCard con jira_key, status, last_sync, last_error, link 'Abrir en Jira'.
  Poll cada 10s, refresh-tick opcional.
- KanbanCard: agrupa menu + indicator en Stack vertical (indicator debajo de los 3 dots).
- ImportJiraModal: modal admin con tabla de issues. Checkbox por fila, filtro por texto,
  toggle 'mostrar ya importadas', Select de columna fallback. Tras import recarga board.
- App.tsx: nueva entrada de menu 'Importar de Jira' (admin) y ImportJiraModal mounted.

Backend tests siguen verdes (test mock cubre transitions endpoints).
Frontend pnpm build OK.
2026-05-29 12:00:26 +02:00
egutierrez 5744b82f58 feat(jira): issue_type config + labels_map + status_map default DATA + transition tras create
- jiraConfig: campos IssueType + LabelsMap (kanban col -> labels Jira). Default
  IssueType='Tarea Tecnica' (DATA project no tiene Task).
- create(): usa c.IssueType y aplica labels iniciales. Despues del POST /issue
  ejecuta transitionToStatus para mover la card recien creada al status del
  status_map, asi no aterriza en el initial workflow status (CREADO o To Do)
  sino donde toca segun la columna kanban.
- update() y transition(): aplican labels en cada sync (PUT replaces array).
  Card que sale de Bloqueadas pierde el label 'blocked' automaticamente.
- transitionToStatus: helper compartido entre create() y transition().
- seed-jira-data: inyecta status_map por defecto para nuestras 6 columnas
  (HACIENDO -> In Progress, PNDNT FEEDBACK -> IMPLEMENTADO, HECHO -> Done,
  IDEAS -> CREADO, DEUDA TECNICA -> To Do, Bloqueadas -> In Progress) y
  labels_map (Bloqueadas -> ['blocked']).
- modules_test: mock Jira tambien responde /transitions endpoints.
2026-05-29 11:44:04 +02:00
egutierrez ef197236db feat(modules): jira scoped a project=DATA + board=33 con seed CLI desde pass
Cambios:
- jiraConfig: nuevo campo BoardID. TestConnection valida que board.location.projectKey
  coincide con ProjectKey declarado. Refuse mismatched scopes so a typo in
  project_key cannot create issues in the wrong project.
- backend/seed_jira.go: subcomando 'kanban seed-jira-data' lee credenciales
  desde pass (jira/anjana/{email,api-token,domain}) e inserta module row con
  kind=jira, project_key=DATA, board_id=33, event_filter sensible. Idempotente
  (upsert por name). status_map vacio por defecto (operator lo edita por UI).
- main.go: wire del nuevo subcomando.

Requiere KANBAN_MODULE_KEY env var para encriptar/desencriptar config. El
servidor que ejecuta el dispatcher debe usar el mismo valor.
2026-05-28 12:56:33 +02:00
egutierrez 65771ebb12 feat(mcp): mint-token CLI + get_card / delete_comment tools + executeToolAs(actor)
Net-new capacidades recuperadas del WIP stash que el merge notif no traia:

- mint-token CLI subcommand: 'kanban mint-token --user <id> --name <pc>' genera token bearer
  para configurar Claude Code u otros clientes MCP HTTP sin tocar la UI.
- executeToolAs(db, name, input, actor): variante actor-aware de executeTool. El dispatcher
  HTTP /mcp pasa el user_id resuelto del bearer token; tools per-user (add_comment,
  delete_comment) lo usan como autor sin que el llamante pueda forjarlo.
- get_card tool: lookup por id o seq_num. Devuelve Card completa.
- delete_comment tool: borra card_message; solo el autor original (validado en DB).

executeTool() sigue siendo el wrapper legacy sin actor para chat WS.
2026-05-28 09:36:48 +02:00
egutierrez 084defe014 Merge branch 'merge/notifications-modules' into master
Trae notifications-realtime + modules + MCP tokens al master preservando
files attachments (issue 0128). Migrations renumeradas: 014_card_files (master),
015_notifications, 016_modules, 017_mcp_tokens (notif renumerada).

Bump version 0.2.0 -> 0.5.0.
2026-05-27 18:50:44 +02:00
egutierrez abb787facd chore: go mod tidy tras merge notif (nuevas deps de modules/notifications) 2026-05-27 18:50:30 +02:00
egutierrez 6a35bdec42 chore: auto-commit (5 archivos)
- backend/dist/assets/index-DFLRkdHe.js
- backend/dist/assets/index-DT3pghXY.js
- backend/dist/assets/index-UVzY_37O.js
- backend/dist/index.html
- frontend/src/components/CardChatPanel.tsx

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:48:14 +02:00
egutierrez 065070cec7 fix(tests): card_history tool ahora retorna *CardHistoryResponse — desempaquetar .ColumnHistory 2026-05-27 18:46:18 +02:00
egutierrez 172850178d merge: bring notifications-realtime + modules into master (preserves files attachments) 2026-05-27 18:43:54 +02:00
egutierrez d13993c0d7 chore(migrations): renumber 014/015/016 -> 015/016/017 to avoid collision with master 014_card_files 2026-05-27 18:38:46 +02:00
egutierrez 5b30fb1ded chore: restore control.sh TUI launcher from issue/notifications-realtime
Script vivia solo en rama issue/notifications-realtime y se perdio al
hacer checkout master para el branch 0128. Es self-contained (no toca
otros archivos de esa rama). Permite ./control.sh para gestionar
backend (WSL) + frontend Vite (Windows).
2026-05-27 11:13:03 +02:00
dataforge 87fd95314e Merge pull request 'feat(kanban): adjuntos de archivos por card (issue 0128)' (#1) from issue/0128-files-attachments into master 2026-05-27 09:04:38 +00:00
egutierrez 472fa25bae build(frontend): rebuild dist with XSS scheme guard 2026-05-27 11:04:20 +02:00
egutierrez aab4f12fc4 fix(0128): XSS scheme allowlist + drop dead fileID
review findings:
- MessageBody: only http(s) and relative paths allowed for links;
  data:image/* allowed for inline images. Rejects javascript:,
  data:text/html, vbscript: which would execute via <a href>.
  Unsafe matches fall back to plain text.
- files.go: remove unused fileID var generated then discarded.
2026-05-27 11:04:20 +02:00
egutierrez e86c93cb73 build(frontend): rebuild embedded dist with files UI 2026-05-27 10:52:15 +02:00
egutierrez 489d2bbef6 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
2026-05-27 10:52:06 +02:00
egutierrez ac5f016e7e feat(frontend): UI archivos en cards (issue 0128)
- CardFilesPanel: tab Archivos con grid thumbs + boton subir/borrar
- CardForm: drag&drop en descripcion, inserta ref markdown en cursor
- CardChatPanel: drag&drop + boton paperclip, sube y envia ref como mensaje
- MessageBody: renderer markdown minimo (img inline + link chip)
- api.ts: listCardFiles, uploadCardFile (multipart), deleteCardFile
- types.ts: CardFile
2026-05-27 10:52:01 +02:00
egutierrez 2401eb5abc feat(backend): card file attachments (issue 0128)
- migration 014_card_files: tabla con soft-delete + index activo
- handlers POST/GET/DELETE en backend/files.go
- routes /api/cards/{id}/files, /api/files/{id}
- limite 10MB, storage en uploads/<card_id>/<random>__<safe>
2026-05-27 10:51:52 +02:00
egutierrez 12729b5166 chore: auto-commit (2 archivos)
- backend/mcp.go
- backend/tools.go

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:35:39 +02:00
egutierrez c28ae7d3c0 chore: auto-commit (12 archivos)
- app.md
- backend/handlers.go
- backend/main.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/vite.config.ts
- backend/mcp_http.go
- backend/mcp_tokens.go
- backend/mcp_tokens_handlers.go
- backend/migrations/016_mcp_tokens.sql
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:38:17 +02:00
egutierrez c9e15513c7 chore: auto-commit (23 archivos)
- app.md
- backend/dist/assets/index-CFDWXN9Z.js
- backend/dist/index.html
- backend/handlers.go
- backend/main.go
- backend/users.go
- e2e/smoke_live.sh
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/CardChatPanel.tsx
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:22:44 +02:00
egutierrez 2524340759 chore: auto-commit (21 archivos)
- app.md
- backend/dist/assets/index-D_Kep7Fb.js
- backend/dist/index.html
- backend/handlers.go
- backend/main.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/CardChatPanel.tsx
- frontend/src/components/LoginPage.tsx
- frontend/src/types.ts
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:04 +02:00
egutierrez 1923fd31a4 chore: auto-commit (1 archivos)
- registry.db

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:37:33 +02:00
egutierrez b599090876 chore: auto-commit (1 archivos)
- app.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:32:55 +02:00
egutierrez 69a0d351fc Merge issue 0094: kanban daily summary + pdf 2026-05-14 18:08:09 +02:00
egutierrez 9c5e76e03f feat(kanban): bocadillo agente + PDF descargable en reporte diario (issue 0094)
Anade tres capas sobre el reporte diario del issue 0093:

1) Bocadillo del agente: cuadro azul encima de "Tareas hechas" con un
   resumen en lenguaje natural (max 4 frases) generado por claude -p
   sobre el JSON del reporte. Botones Regenerar e icono Settings.

2) Settings del prompt: modal con textarea editable para el template
   del agente (key=daily_report_prompt). Compartido por todos los
   usuarios. Boton Restablecer por defecto.

3) PDF descargable: boton que abre ventana nueva con HTML imprimible
   (estilo A4, KPIs filtrados, tabla con enlaces absolutos por card).
   Permite compartir el listado de tareas hechas con los solicitantes.

Backend:
- Migration 013 anade tablas daily_summaries y settings; seed del
  prompt por defecto en castellano.
- daily_summary.go con GetSetting/SetSetting, GetDailySummary/Upsert,
  runClaudePrompt (envuelve claude -p) y GenerateDailySummary que
  orquesta DailyReportFor + plantilla + claude + persist.
- Nuevos endpoints:
  * GET  /api/reports/daily/summary
  * POST /api/reports/daily/summary
  * GET  /api/settings/{key}
  * PUT  /api/settings/{key}

Frontend:
- api.ts: getDailySummary, generateDailySummary, getSetting, setSetting.
- DailyReport.tsx: estado de summary, settingsOpen, promptDraft,
  filterRequester, filterAssignee, filteredDoneCards, exportPDF.
- Bocadillo con IconSparkles + IconRefresh + IconSettings.
- Modal de prompt con Guardar/Cancelar/Reset.
- Filtros Select por solicitante y asignado encima de la tabla.
- exportPDF abre window.open con HTML self-contained que incluye
  enlaces ${origin}/?card=${id} y window.print() automatico.

E2E nuevo (daily-summary-pdf.spec.ts): CRUD del setting, GET summary
shape, presencia del boton PDF/Settings/Regenerar en el modal. No
invoca claude real (binario externo, no disponible en CI).

Suite completa 11/11 pasa.
2026-05-14 18:08:09 +02:00
egutierrez fc7e6a34a7 feat(kanban): reporte diario al click en dia del calendario (issue 0093)
Adds a daily report dashboard accessible by clicking a day number in the
calendar view. Renders inside a full-width modal (90% width).

Backend (new file backend/reports.go):
- Type DailyReport with KPIs, rankings, done_cards list, reopened cards,
  3-bucket stale list (7/14/30d), lead time avg+p50+p95, 24-hour
  movement histogram, deadlines met/missed list, tag distribution and
  archived count.
- DB.DailyReportFor(date, tz) uses Europe/Madrid by default; computes
  [start,end) in local time, converts to UTC and queries:
  * cards.completed_at in range  -> done list
  * card_events kind=created in range -> created counts
  * card_column_history.entered_at in range -> moves + hourly
  * previousColumnWasDone() -> reopened detection
  * card_lock_history overlapping the day -> blocked_ms
  * stale buckets: open history entries on non-done columns aged >=7d
- New route GET /api/reports/daily?date=YYYY-MM-DD&tz=Europe/Madrid.

Frontend:
- api.ts: DailyReport type + dailyReport(date, tz?) call.
- New component DailyReportView (components/DailyReport.tsx):
  * 6 KPI cards (Hechas, Creadas, Movimientos, Bloqueado, Reabiertas,
    Deadlines on-time %).
  * 4 ranking cards (Top assignees done, Top assignees created,
    Top requesters atendidas, Top requesters aportadas).
  * Done cards table with click-to-jump (links open the card in board).
  * Mantine BarChart with movements per hour.
  * Tag chips, reopened list, deadlines list with late_ms, stale buckets.
- CalendarView wraps the day number in UnstyledButton with data-test
  attribute and forwards onOpenDailyReport.
- App.handleOpenDailyReport opens modals.open size 90% with the view;
  click on a card title closes the modal and jumps to the board with
  highlight (reuses existing handleJumpToCard).

Tests (e2e/daily-report.spec.ts):
- Endpoint shape: kpis, done_cards, hourly_moves[24], stale buckets.
- Calendar day click opens the modal with "Reporte diario" title and
  KPI labels visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 9d3ab5f0f3 feat(kanban): hide "Seleccionar Aleatorio" in done columns
The random-pick menu entry is meaningless for done columns — cards there
are already finished and now get auto-archived after 30 days (issue
0092). Gate the Menu.Item on !column.is_done so the action only appears
in active columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 9b503f0555 feat(kanban): archive automatico para cards Done +30 dias (issue 0092)
Adds an archive layer separate from the trash. Cards in is_done columns
that have been there for more than 30 days are auto-archived on the next
board load (throttled to once every 30 minutes). Archived cards leave
the board but stay in the DB and are listed in a new sidebar drawer
"Hecho (archivo)" below the existing Papelera, with a one-click restore.

Schema (migration 012_card_archived.sql):
- ALTER TABLE cards ADD COLUMN archived_at TEXT;
- NULL = active, ISO timestamp = archived. Independent from deleted_at.

Backend:
- Card.ArchivedAt + JSON; ListCardsWithTime filters archived_at IS NULL.
- New methods: ArchiveCard, UnarchiveCard, ListArchivedCards,
  AutoArchiveDoneOlderThan.
- New endpoints: GET /api/archive, POST /api/cards/:id/archive,
  POST /api/cards/:id/unarchive.
- handleGetBoard invokes maybeAutoArchive (atomic throttle, 30 min sweep,
  30 day cutoff). Errors logged but never block the board response.

Frontend:
- Card type + api.ts add the new field and helpers.
- App.tsx state for archive list, reload, archive/unarchive handlers.
- New sidebar drawer with toggle, count badge, restore button.
- KanbanCard gains an "Archivar" menu item (gated on isDone +
  onArchive prop) for manual archiving of any done card.

Tests:
- Playwright e2e/archive.spec.ts: manual archive via menu, drawer
  toggle, unarchive. Picks a done card via /api/board introspection so
  it stays stable regardless of board state.
- Auto-archive of >30d cards: not under e2e (real time travel needed);
  covered by code review of the SQL query and the throttle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez c4caff85be perf(kanban): split KanbanCard body into memoized child (dnd lag fix)
Drag perf measured via new Playwright spec drag-perf.spec.ts which drives
a slow drag across the biggest column (~35 cards) while capturing per-frame
durations via rAF inside the page. Pre-fix metrics in HECHO column:

  wrapper-renders=1942 body-renders=N/A
  p50=16.7ms p95=83.3ms max=116.7ms (12fps stalls)

Root cause: useSortable inside KanbanCardImpl subscribes to dnd-kit context;
every pointermove during a drag re-renders ALL cards in the SortableContext.
With the old monolithic component, each re-render rebuilt the full Stack +
Menu + 4 Popovers JSX tree — even though no data had changed.

Fix: split KanbanCardImpl into a thin outer (useSortable + Paper wrapper +
sticker overlay handler + style) and a memoed KanbanCardBody (Stack +
sticker overlay + popover state). All popover/requesterDraft local state
lives inside the body now, so its props are stable across drag and
React.memo skips the body work entirely.

Post-fix metrics:

  wrapper-renders=1943 body-renders=0
  p50=16.7ms p95=16.8ms max=50.0ms (steady 60fps with a single 33ms spike)

E2E thresholds tightened: p50<20, p95<50, max<60, body-renders<5. Regression
in any of these will fail CI.

Probe helpers (_probeRender / _probeBodyRender) are no-ops unless
window._cardRenderProbe is set. Production cost: ~3ns per render call.

Also: `now` clock interval already pauses while dragging (previous commit
e656e8c). `animateLayoutChanges:() => false` kept; it does not visibly
change reorder UX with this codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 7ba18f9114 fix(kanban): infinite ref-merge loop + drag lag
Two issues:

1. "Maximum update depth exceeded" inside Mantine's useMergedRef during
   drag. Root cause: the `data` object passed to dnd-kit's `useSortable`
   in KanbanCard and KanbanColumn was re-created on every render. Mantine
   Paper composes its internal ref with the consumer ref via
   useMergedRef, which uses a useCallback whose deps array contains the
   refs themselves. Whenever the underlying setNodeRef from useSortable
   became unstable (because dnd-kit's internal state churned on data
   identity change), the merged ref was reassigned each commit -> setState
   inside the ref callback -> next render -> new data identity -> loop.
   Wrap the sortable data in useMemo keyed on its real inputs.

2. Drag feels laggy. Each card listens to a 1-second `now` clock that
   re-renders the entire board to refresh the "time in column" label.
   Pause that interval while a drag is active so dnd-kit's per-pixel
   reconciliation does not also re-mount/re-layout every card every
   second. Tick resumes the moment the drag ends.

Also move the Select `data` array for the column max-time popover from
an inline expression to a module-level constant; same array identity
across all column instances and renders -> Mantine Combobox stops
re-running its diffing effect for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 76d85959f1 feat(kanban): sidebar edge zone now toggles (open + close)
The 32px left drag zone now also closes the sidebar when it is open.
Symmetric behaviour: dwell ≥400ms while dragging closes if open / opens
if closed.

To prevent a drag that starts with the pointer already inside the
sidebar (e.g. dragging a sidebar card itself) from immediately auto-
closing, the close action requires the pointer to have left the strip
at least once after the drag started. So:
- closed + drag-to-edge -> opens (unchanged).
- open + drag a card out, then move the card back to the edge -> closes.
- open + drag a sidebar card directly to the board -> nothing happens.

After a successful toggle the dwell flag resets, so the user must leave
the strip and re-enter to trigger another action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 257858a1f3 feat(kanban): drag-aware sidebar dropzone (issue 0091)
Add a 32px invisible strip on the left edge of the viewport that
auto-opens the sidebar when the user drags a card and dwells near the
edge for >=400ms. Removes the manual toggle step when moving cards to
sidebar-located columns.

- App.tsx: global mousemove listener while drag is active; 400ms hover
  timer; sets navOpen(true) when triggered; cancels on pointer leave or
  drag end. No auto-close on drag end (user keeps sidebar open).
- dropzone.css: subtle inset blue glow with pulse animation while
  pointer is inside the strip and a drag is active.
- KanbanColumn.tsx: add data-column-id and data-column-location to the
  Paper root for stable e2e selectors.
- e2e/sidebar-dropzone.spec.ts: Playwright test driving a slow drag
  to the left edge, asserting the strip arms, sidebar opens, and the
  card moves to a sidebar column via /api/board.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 30def13c55 feat(kanban): mejoras historial card — DONE check + tiempo por columna
HistoryModal now receives the board's columns via prop to enrich
history events with column metadata.

Timeline:
- Entries for column moves into a column with is_done=true now render
  with a green IconCheck bullet and the kind label "Hecho en columna"
  instead of the generic blue arrows / "Mueve a columna". Makes the
  card's "done" moments scannable at a glance.

Footer (below the timeline):
- Replaces the single Group-of-badges with a structured table showing
  one row per visited column: name, visits (entry count) and total time
  in column. DONE columns are flagged with a green check + bold.
- Total locked time keeps the same source (total_locked_ms) but moved
  to a header line above the table to declutter.
- Currently-active entry (exited_at=null) contributes now - entered_at
  to its row, so the table reflects live time.

App.tsx passes columns from board state when opening the history modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez bc502df48a feat(kanban): tiempo maximo via popover con unidad (issue 0089 followup)
Replace the native window.prompt with a Popover that mirrors the deadline
picker pattern: NumberInput + unit Select (minutos/horas/dias/semanas/meses).
The selected unit converts to minutes at save time; the column's stored
unit on the backend stays unchanged (max_time_minutes).

On open the popover pre-selects the largest unit that yields a clean
integer for the current value (e.g. 1440 -> 1 dia, 60 -> 1 hora).

Includes a trash icon to clear the limit and a Guardar button.
data-test selectors added for future e2e:
- column-max-time, column-max-time-input, column-max-time-unit,
  column-max-time-save.

Menu label now shows "(N dias)" / "(N semanas)" / etc. instead of "(N min)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez c93ac46c37 feat(kanban): Seleccionar Aleatorio en columna con ruleta (issue 0090)
Adds a column-level "Seleccionar Aleatorio" context menu entry that picks
a card at random from the column with a roulette-style highlight
animation that decelerates to the winner.

Selection respects existing filters (uses cardsByColumn which is the
post-filter view) and always excludes locked cards. Menu entry is
disabled when nothing pickable is left.

Implementation:
- KanbanColumn: new Menu.Item with IconDice5; data-test selector for e2e.
- onPickRandom prop wired from KanbanColumn -> App.
- handlePickRandom in App.tsx: cryptographically random winner via
  crypto.getRandomValues, 2 full laps + offset, cubic decay 50ms -> 220ms,
  follows the active card with scrollIntoView.
- src/styles/roulette.css: .kanban-roulette-active (blue pulse, single
  step) and .kanban-roulette-winner (green pulse + scale, ~1.6s).
  Imported globally from main.tsx.

Manual verification only (visual timing + needs real cards). Backend
untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez 9f4fd85db3 feat(kanban): tiempo maximo por columna con borde rojo (issue 0089)
Adds column-level max time limit. Cards whose time_in_column_ms exceeds
the limit show a red border + red halo. Columns marked as Done never
trigger the visual regardless of the limit (per spec).

Backend:
- Migration 011_column_max_time.sql adds columns.max_time_minutes
  INTEGER NOT NULL DEFAULT 0 (0 = no limit). Aditiva, idempotente.
- Column struct + ColumnPatch + UpdateColumn handle the new field;
  negatives clamp to 0; listing query includes it.
- handleUpdateColumn (PATCH /api/columns/:id) accepts max_time_minutes
  in the JSON body.

Frontend:
- Column TS interface + UpdateColumnInput updated.
- KanbanColumn context menu: new entry "Tiempo maximo" using
  window.prompt for low-friction config; shows current value when >0.
- KanbanCard receives columnOverdue prop calculated from the column
  state and card.time_in_column_ms; renders red border (var
  --mantine-color-red-6) with 2px width + 2px red halo when overdue.
- data-card-id, data-column-overdue, data-locked attributes on the card
  paper element so e2e tests / scripts can query state.

Tests: TestColumnMaxTimeMinutes_Defaults + _Update verify the schema
default, the clamp on negative input, and that updating max_time leaves
other fields untouched.

Visual regression of the red border kept out of automated e2e because
it requires either clock control or real cards aged > N minutes; will be
verified manually after merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00
egutierrez eb1c13d82c feat(kanban): requester input empty + keyboard nav (issue 0088)
CardForm: drop pre-fill of requester from logged user; Enter inside the
Autocomplete no longer submits the form (Mantine handles dropdown
selection; arrows + Enter pick option without closing modal). Submit
remains via "Crear" button or Ctrl+Enter from description.

Adds data-field="requester" and data-test="add-card" selectors for stable
e2e queries.

Tests:
- vitest component test (CardForm.test.tsx): empty input, Enter does not
  submit, submit only via button. Dropdown arrow nav covered by e2e
  (jsdom portal handling is brittle).
- Playwright e2e (requester-input.spec.ts) using new browser capability
  group (pw_kanban_login, pw_keyboard_sequence) from registry.
- seed_e2e_user CLI to create deterministic test user against
  operations.db (bcrypt via standard backend hash).

Setup additions (frontend/):
- vitest + @testing-library + jsdom devDeps
- @playwright/test devDep + playwright.config.ts
- src/test/setup.ts polyfills jsdom for Mantine (matchMedia,
  visualViewport, document.fonts, ResizeObserver)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:57:00 +02:00
egutierrez a34a8142cc chore: auto-commit (23 archivos)
- app.md
- backend/auth.go
- backend/db.go
- backend/dist/assets/index-CPqSy0gZ.js
- backend/dist/index.html
- backend/handlers.go
- backend/main.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/KanbanCard.tsx
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:40:22 +02:00
egutierrez f1ee116d3b chore: auto-commit (5 archivos)
- app.md
- backend/chat.go
- Dockerfile
- docker-compose.yml
- traefik-dynamic.yml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:11:24 +02:00
egutierrez ce807ec2ee Merge quick/fix-ws-proxy: vite ws proxy + chat WS e2e 2026-05-09 15:00:41 +02:00