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>
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>
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>
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>
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>
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>
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>
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>
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>
- badges: locked → tiempo bloqueado; done → fecha completion + total lead time; otherwise → tiempo en columna
- locked cards: drag permitido dentro de mismo column (cross-column rejected con notification)
- card field: locked_at desde JOIN card_lock_history (open period)
- migrations: refactor a embed.FS, archivos 002-005 extraidos de ensureColumns; ensureColumns queda como backstop
- stickers UX: opacidad 1, debajo del texto, picker estable (useRef), boton entra directo a modo con 😀, popover cierra outside, cards done filter brightness
- format: formatDateTimeShort
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>