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.
This commit is contained in:
egutierrez
2026-05-29 15:01:51 +02:00
parent c5113f75a5
commit 9b0b6e516c
6 changed files with 209 additions and 151 deletions
+33 -12
View File
@@ -5,11 +5,11 @@ import * as api from "../api";
import type { CardJiraSyncState } from "../api";
import { formatDateTimeShort } from "./format";
// Pull state every POLL_MS so the indicator catches up with async dispatcher
// pushes without needing SSE. 10s is a reasonable balance: short enough that a
// drag-to-Jira shows green within one tick, long enough that the polling is
// not a noticeable load.
const POLL_MS = 10000;
// Adaptive polling: 5s steady-state, 1s while the card is mid-sync. The fast
// cadence catches the yellow → green transition right after a column drag;
// the slow cadence keeps the per-card load manageable when the board is idle.
const POLL_MS_STEADY = 5000;
const POLL_MS_INFLIGHT = 1000;
type Tone = "gray" | "yellow" | "green" | "red";
@@ -47,22 +47,43 @@ export function JiraSyncIndicator({ cardId, refreshTick }: Props) {
useEffect(() => {
let cancelled = false;
let timer: ReturnType<typeof setTimeout> | null = null;
const schedule = (ms: number) => {
if (cancelled) return;
if (timer) clearTimeout(timer);
timer = setTimeout(load, ms);
};
const load = async () => {
try {
const s = await api.getCardJiraSync(cardId);
if (!cancelled) {
setState(s);
setErr(null);
}
if (cancelled) return;
setState(s);
setErr(null);
// Adaptive cadence: fast while the dispatcher is actively processing
// this card, slow otherwise. Re-arms on every fetch so the moment
// inflight flips off we drop back to the steady cadence.
schedule(s.inflight ? POLL_MS_INFLIGHT : POLL_MS_STEADY);
} catch (e) {
if (!cancelled) setErr((e as Error).message);
if (cancelled) return;
setErr((e as Error).message);
schedule(POLL_MS_STEADY);
}
};
// Window event fired by the App's drag-end handler so the indicator
// refetches immediately after a move (and so the user sees yellow within
// ~200ms instead of waiting up to POLL_MS_STEADY).
const onMoved = (e: Event) => {
const detail = (e as CustomEvent).detail as { cardId?: string } | undefined;
if (detail?.cardId === cardId) {
schedule(150);
}
};
window.addEventListener("kanban-card-moved", onMoved);
load();
const t = setInterval(load, POLL_MS);
return () => {
cancelled = true;
clearInterval(t);
if (timer) clearTimeout(timer);
window.removeEventListener("kanban-card-moved", onMoved);
};
}, [cardId, refreshTick]);