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:
@@ -224,3 +224,44 @@ func handleTestModule(db *DB, dispatcher *Dispatcher) http.HandlerFunc {
|
||||
infra.HTTPJSONResponse(w, http.StatusOK, resp)
|
||||
})
|
||||
}
|
||||
|
||||
// handleCardJiraSync returns the per-card Jira sync state for the indicator
|
||||
// tooltip. Reads cards.jira_last_* columns + dispatcher inflight map. The
|
||||
// caller does not need admin: any authenticated user can see the state of
|
||||
// their cards. Returns 200 + zero-valued state when the card has no link
|
||||
// yet (so the UI can show the gray indicator without a special case).
|
||||
func handleCardJiraSync(db *DB, dispatcher *Dispatcher) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
|
||||
if uid == "" {
|
||||
infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "session required"})
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
state, err := db.readCardJiraSync(id)
|
||||
if err != nil {
|
||||
notFound(w, "card not found")
|
||||
return
|
||||
}
|
||||
state.Inflight = dispatcher.IsInflight(id)
|
||||
// Resolve issue URL by reading any enabled jira module's base_url. We
|
||||
// pick the first match because the kanban-jira link is conceptually
|
||||
// 1:1 — multiple jira modules pointing at different projects would be
|
||||
// a misconfiguration.
|
||||
if state.JiraKey != "" {
|
||||
if mods, err := db.listModulesEnabled(); err == nil {
|
||||
for _, m := range mods {
|
||||
if m.Kind != "jira" {
|
||||
continue
|
||||
}
|
||||
cfg, perr := parseJiraConfig(m)
|
||||
if perr == nil && cfg.BaseURL != "" {
|
||||
state.IssueURL = cfg.BaseURL + "/browse/" + state.JiraKey
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
infra.HTTPJSONResponse(w, http.StatusOK, state)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user