Migración a Python/Flask y adaptación para proyecto Infra

- Reemplaza Netlify Functions con servidor Python/Flask (server.py)
- Adapta dashboard para contar tickets en lugar de Story Points
- Conecta al proyecto IO (Infra) de Jira
- Elimina secciones de QA y Code Review
- Agrega métricas de solicitudes semanales y reporters
- Excluye devs inactivos y unifica nombres duplicados

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
juan.beldomenico
2026-04-27 15:09:04 -03:00
parent 76edc176f9
commit 695401f237
7 changed files with 711 additions and 427 deletions
+34 -16
View File
@@ -8,7 +8,7 @@
const config = {
jira: {
host: process.env.JIRA_HOST || 'invera.atlassian.net',
project: process.env.JIRA_PROJECT || 'LABS',
project: process.env.JIRA_PROJECT || 'Infra',
email: process.env.JIRA_EMAIL,
token: process.env.JIRA_TOKEN,
},
@@ -30,7 +30,7 @@ const config = {
enCursoEntry: 'customfield_10815',
enCursoTotalTime: 'customfield_10816',
},
jql: process.env.JIRA_JQL || 'project = ${PROJECT} AND ${FIELD_SP_ESTIMATE} is not EMPTY ORDER BY updated DESC',
jql: process.env.JIRA_JQL || 'project = ${PROJECT} AND issuetype not in (Epic, "Sub-task", Subtask) ORDER BY updated DESC',
api: {
cacheMsec: parseInt(process.env.CACHE_MS || '3600000'), // 1 hora por defecto
},
@@ -244,7 +244,37 @@ function processIssues(issues, period = '90d') {
}
}
issuesList.push({ id: issue.id, key: issue.key, summary: f.summary, status: statusKey, sp, devs, creMonth });
const issueWarnings = [];
if (isInProgress && f.updated) {
const days = (now - new Date(f.updated)) / (1000 * 60 * 60 * 24);
if (days > 14) {
const w = { type: 'stalled', key: issue.key, summary: f.summary, devs, sp, days: Math.round(days) };
warnings.push(w);
issueWarnings.push(w);
}
}
if (!isDone && f.duedate) {
const dueDate = new Date(f.duedate);
if (dueDate < now) {
const daysOverdue = Math.floor((now - dueDate) / (1000 * 60 * 60 * 24));
const w = { type: 'expired', key: issue.key, summary: f.summary, devs, sp, days: daysOverdue };
warnings.push(w);
issueWarnings.push(w);
}
}
if (isDone && sp === 0) {
const w = { type: 'done_no_sp', key: issue.key, summary: f.summary, devs, sp: 0 };
warnings.push(w);
issueWarnings.push(w);
}
if (!isDone && devs.includes('Sin asignar') && sp > 0) {
const w = { type: 'unassigned_sp', key: issue.key, summary: f.summary, devs, sp };
warnings.push(w);
issueWarnings.push(w);
}
issuesList.push({ id: issue.id, key: issue.key, summary: f.summary, status: statusKey, sp, devs, creMonth, warnings: issueWarnings });
const timeQaMin = f[config.fields.qaTotalTimeQa] ?? 0;
const timeRtqaMin = f[config.fields.qaTotalTimeRtqa] ?? 0;
@@ -305,19 +335,7 @@ function processIssues(issues, period = '90d') {
}
}
if (isInProgress && f.updated) {
const days = (now - new Date(f.updated)) / (1000 * 60 * 60 * 24);
if (days > 14) warnings.push({ type: 'stalled', key: issue.key, summary: f.summary, devs, sp, days: Math.round(days) });
}
if (!isDone && f.duedate) {
const dueDate = new Date(f.duedate);
if (dueDate < now) {
const daysOverdue = Math.floor((now - dueDate) / (1000 * 60 * 60 * 24));
warnings.push({ type: 'expired', key: issue.key, summary: f.summary, devs, sp, days: daysOverdue });
}
}
if (isDone && sp === 0) warnings.push({ type: 'done_no_sp', key: issue.key, summary: f.summary, devs, sp: 0 });
if (!isDone && devs.includes('Sin asignar') && sp > 0) warnings.push({ type: 'unassigned_sp', key: issue.key, summary: f.summary, devs, sp });
// Las alertas de salud se generan y empujan arriba (junto con issueWarnings)
} catch (e) {
console.warn(`Error procesando issue ${issue.key}: ${e.message}`);