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.
This commit is contained in:
egutierrez
2026-05-29 12:00:26 +02:00
parent 5744b82f58
commit c3cc42b350
13 changed files with 2450 additions and 1330 deletions
+19
View File
@@ -57,6 +57,7 @@ import {
IconLogout,
IconPlug,
IconKey,
IconBrandJira,
IconMenu2,
IconMessageChatbot,
IconMoodSmile,
@@ -86,6 +87,7 @@ import { colorBg, colorBorder } from "./components/colors";
import { NotificationsBell } from "./components/NotificationsBell";
import { ModulesModal } from "./components/ModulesModal";
import { MCPTokensModal } from "./components/MCPTokensModal";
import { ImportJiraModal } from "./components/ImportJiraModal";
import { useEventStream } from "./hooks/useEventStream";
import type { Board, Card, CardColor, Column, ColumnLocation, Notification, User } from "./types";
@@ -364,6 +366,7 @@ export function App() {
const [modulesOpen, setModulesOpen] = useState(false);
const [mcpTokensOpen, setMcpTokensOpen] = useState(false);
const [jiraImportOpen, setJiraImportOpen] = useState(false);
const reloadNotifs = useCallback(async () => {
try {
@@ -1292,6 +1295,14 @@ export function App() {
Modulos
</Menu.Item>
)}
{auth.user.is_admin && (
<Menu.Item
leftSection={<IconBrandJira size={14} />}
onClick={() => setJiraImportOpen(true)}
>
Importar de Jira
</Menu.Item>
)}
<Menu.Item
leftSection={<IconKey size={14} />}
onClick={() => setMcpTokensOpen(true)}
@@ -1311,6 +1322,14 @@ export function App() {
{auth.user?.is_admin && (
<ModulesModal opened={modulesOpen} onClose={() => setModulesOpen(false)} />
)}
{auth.user?.is_admin && board && (
<ImportJiraModal
opened={jiraImportOpen}
onClose={() => setJiraImportOpen(false)}
columns={board.columns}
onImported={() => reload()}
/>
)}
<MCPTokensModal opened={mcpTokensOpen} onClose={() => setMcpTokensOpen(false)} />
</Group>
</Group>