commit 4183bb8313fe3da2405ddfb07d58e9b095bbed6b Author: kvilches1410 Date: Tue Mar 31 12:13:19 2026 -0300 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..59dbff9 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +JIRA_EMAIL=kevin.vilches@invera.com.ar +JIRA_TOKEN=ATATT3xFfGF0tU8kLrJ9MNe4_QMk8GnLrr-xnvCity-1W2Uvnb90eBu_gdTHxYwjdpnSMFW4LdxxF_KF_39N8jjsSGlranWZmYKTisJqRjYIxQ-cYzgRe9ZOLJUheMNYeyWBi4YvgcHeSPnnDt7X69d4-vEzUFs-SKtWwocElrbc6C0A83YgcE8=F3FF6B2B diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e985853 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/api/data.js b/api/data.js new file mode 100644 index 0000000..8a5fcbc --- /dev/null +++ b/api/data.js @@ -0,0 +1,122 @@ +// api/data.js — Vercel Serverless Function +// Consulta Jira en tiempo real y devuelve los datos procesados del dashboard. +// Variables de entorno requeridas: JIRA_EMAIL, JIRA_TOKEN +const JIRA_HOST = 'invera.atlassian.net'; +const JQL = 'project = LABS AND cf[10016] is not EMPTY ORDER BY updated DESC'; +const FIELDS = [ + 'summary', 'status', 'assignee', + 'customfield_10016', // Story point estimate + 'customfield_11019', // Story Points Finales + 'customfield_10622', // Desarrollador (multi-user) + 'parent', 'resolutiondate', 'created', +].join(','); +const MAX_RESULTS = 100; +const CACHE_MS = 24 * 60 * 60 * 1000; // 24 horas +// Cache en memoria (se reinicia con cada cold start de la función) +let _cache = null; +let _cacheTime = 0; +// ─── Jira fetch ────────────────────────────────────────────────────────────── +async function fetchJiraIssues(email, token) { + const auth = Buffer.from(`${email}:${token}`).toString('base64'); + const url = `https://${JIRA_HOST}/rest/api/3/search/jql` + + `?jql=${encodeURIComponent(JQL)}` + + `&fields=${encodeURIComponent(FIELDS)}` + + `&maxResults=${MAX_RESULTS}`; + const res = await fetch(url, { + headers: { + 'Authorization': `Basic ${auth}`, + 'Accept': 'application/json', + }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Jira API ${res.status}: ${text.slice(0, 200)}`); + } + return res.json(); +} +// ─── Data processing ───────────────────────────────────────────────────────── +function processIssues(issues) { + const assigneeData = {}; + const monthlyDone = {}; + const epicMap = {}; + for (const issue of issues) { + const f = issue.fields; + // Desarrollador (customfield_10622 multi-user), fallback a assignee + const devs = f.customfield_10622?.length + ? f.customfield_10622.map(d => d.displayName) + : f.assignee + ? [f.assignee.displayName] + : ['Sin asignar']; + // SP: SP Finales si está cargado, si no el estimate + const sp = (f.customfield_11019 != null ? f.customfield_11019 : f.customfield_10016) ?? 0; + // Estado + const catKey = f.status?.statusCategory?.key ?? 'new'; + // Fechas (YYYY-MM) + const resMonth = f.resolutiondate ? f.resolutiondate.slice(0, 7) : null; + const creMonth = f.created ? f.created.slice(0, 7) : null; + // Épica (parent) + const epic = f.parent?.fields?.summary ?? 'Sin épica'; + // ── Acumular por desarrollador ── + for (const dev of devs) { + if (!assigneeData[dev]) assigneeData[dev] = { done: 0, in_progress: 0, todo: 0 }; + if (catKey === 'done') assigneeData[dev].done += sp; + else if (catKey === 'indeterminate') assigneeData[dev].in_progress += sp; + else assigneeData[dev].todo += sp; + } + // ── Acumular completados por mes ── + if (catKey === 'done') { + const month = resMonth || creMonth; + if (month) { + if (!monthlyDone[month]) monthlyDone[month] = {}; + for (const dev of devs) { + monthlyDone[month][dev] = (monthlyDone[month][dev] ?? 0) + sp; + } + } + } + // ── Acumular por épica ── + if (!epicMap[epic]) epicMap[epic] = { name: epic, done: 0, in_progress: 0, todo: 0 }; + if (catKey === 'done') epicMap[epic].done += sp; + else if (catKey === 'indeterminate') epicMap[epic].in_progress += sp; + else epicMap[epic].todo += sp; + } + return { + lastUpdated: new Date().toISOString(), + totalIssues: issues.length, + assigneeData, + monthlyDone, + epicData: Object.values(epicMap) + .sort((a, b) => (b.done + b.in_progress + b.todo) - (a.done + a.in_progress + a.todo)), + }; +} +// ─── Handler ───────────────────────────────────────────────────────────────── +module.exports = async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET'); + // CDN cache: 24hs fresco, sirve stale mientras recarga + res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate=3600'); + // Force refresh si se pasa ?refresh=1 + const forceRefresh = req.query?.refresh === '1'; + // Servir desde caché si está fresco + if (!forceRefresh && _cache && (Date.now() - _cacheTime) < CACHE_MS) { + return res.status(200).json({ ..._cache, fromCache: true }); + } + const email = process.env.JIRA_EMAIL; + const token = process.env.JIRA_TOKEN; + if (!email || !token) { + return res.status(500).json({ + error: 'Faltan variables de entorno: JIRA_EMAIL y/o JIRA_TOKEN', + hint: 'Configurarlas en Vercel → Settings → Environment Variables', + }); + } + try { + const { issues, total } = await fetchJiraIssues(email, token); + const data = processIssues(issues); + data.jiraTotal = total; // total real en Jira (puede ser > 100) + _cache = data; + _cacheTime = Date.now(); + return res.status(200).json({ ...data, fromCache: false }); + } catch (err) { + console.error('[dashboard/api/data]', err.message); + return res.status(500).json({ error: err.message }); + } +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..74a08b2 --- /dev/null +++ b/index.html @@ -0,0 +1,456 @@ + + + + + +Dashboard LABS — Story Points + + + + + +
+
+

Consultando Jira en tiempo real…

+
+ +
+
+
+

📊 Dashboard LABS — Story Points

+
+ Proyecto: LABS · invera.atlassian.net · Campo: Desarrollador +
+
+
+ — + + +
+
+ +
+
+

Estado Actual del Equipo

+ Tickets activos en el tablero ahora mismo +
+
+
+
+
—
SP En Progreso
En curso · Code Review · QA
+
—
SP Por Hacer
Backlog asignado
+
—
SP Completados (total)
Histórico acumulado
+
—
Devs activos
Con SP asignados
+
+
+
+

🔵 SP en Progreso — por Desarrollador

+
Estado actual · En curso / Code Review / QA
+
+
+
+

📦 Estado actual por Desarrollador

+
Completados · En Progreso · Por Hacer (acumulado)
+
+
+
+
+

Detalle por Desarrollador — Estado Actual

+ + + +
Desarrollador✅ Completados🔵 En Progreso⏳ Por HacerTotal SPProgreso
+
+
+
+ +
+
+

Completados por Período

+ Filtrar por mes de resolución + + + Todos los meses +
+
+
+
+
—
SP Completados
en el período
+
—
Desarrolladores
que completaron SP
+
—
SP promedio / dev
en el período
+
+
+
+

✅ SP Completados — por Desarrollador

+
Período seleccionado
+
+
+
+

📈 Velocidad del Equipo

+
SP completados por mes · cada color = un dev
+
+
+
+
+
+

🗂 SP por Épica — Estado Actual

+
Completados · En Progreso · Por Hacer en cada épica
+
+
+
+
+

SP Completados — Todos los meses

+ + + +
Desarrollador✅ SP Completados% del total del período
+
+
+
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..08837de --- /dev/null +++ b/package.json @@ -0,0 +1,4 @@ +{ + "name": "labs-dashboard", + "version": "1.0.0" +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..dabfde0 --- /dev/null +++ b/vercel.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "routes": [ + { + "src": "/api/(.*)", + "dest": "/api/$1" + }, + { + "src": "/(.*)", + "dest": "/index.html" + } + ] +}