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>
This commit is contained in:
2026-05-14 16:11:59 +02:00
parent c4caff85be
commit 9b503f0555
12 changed files with 1529 additions and 1191 deletions
+3
View File
@@ -82,6 +82,7 @@ interface Props {
onAssignCard: (id: string, assignee_id: string | null) => void;
onSetCardDeadline?: (id: string, deadline: string | null) => void;
onSetRequester?: (id: string, requester: string) => void;
onArchiveCard?: (id: string) => void;
requesterOptions?: string[];
onOpenCustomCardColor?: (cardId: string, current: string) => void;
activeSticker?: string | null;
@@ -117,6 +118,7 @@ function KanbanColumnImpl({
onAssignCard,
onSetCardDeadline,
onSetRequester,
onArchiveCard,
requesterOptions,
onOpenCustomCardColor,
activeSticker,
@@ -593,6 +595,7 @@ function KanbanColumnImpl({
onAssign={onAssignCard}
onSetDeadline={onSetCardDeadline}
onSetRequester={onSetRequester}
onArchive={onArchiveCard}
requesterOptions={requesterOptions}
onOpenCustomColor={onOpenCustomCardColor}
users={users}