@cccarv82/freya 2.13.4 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/web-ui.css +94 -0
- package/cli/web-ui.js +168 -0
- package/cli/web.js +234 -81
- package/package.json +1 -1
package/cli/web-ui.css
CHANGED
|
@@ -1501,6 +1501,100 @@ textarea:focus {
|
|
|
1501
1501
|
animation: fadeIn 0.2s ease-out;
|
|
1502
1502
|
}
|
|
1503
1503
|
|
|
1504
|
+
/* ── Companion Dashboard Tabs ── */
|
|
1505
|
+
.companionTabs {
|
|
1506
|
+
display: flex;
|
|
1507
|
+
gap: 8px;
|
|
1508
|
+
margin: 16px 0;
|
|
1509
|
+
border-bottom: 1px solid var(--border);
|
|
1510
|
+
padding-bottom: 8px;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.companionTabs .tab {
|
|
1514
|
+
padding: 8px 12px;
|
|
1515
|
+
background: transparent;
|
|
1516
|
+
border: none;
|
|
1517
|
+
cursor: pointer;
|
|
1518
|
+
color: var(--text);
|
|
1519
|
+
font-size: 13px;
|
|
1520
|
+
position: relative;
|
|
1521
|
+
transition: color 0.2s ease;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
.companionTabs .tab:hover {
|
|
1525
|
+
color: var(--accent);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.companionTabs .tab.active {
|
|
1529
|
+
color: var(--accent);
|
|
1530
|
+
border-bottom: 2px solid var(--accent);
|
|
1531
|
+
padding-bottom: 6px;
|
|
1532
|
+
margin-bottom: -10px;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/* ── Consolidated View (4 KPI Cards) ── */
|
|
1536
|
+
.consolidatedView {
|
|
1537
|
+
display: grid;
|
|
1538
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1539
|
+
gap: 16px;
|
|
1540
|
+
margin-bottom: 24px;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.kpiCard {
|
|
1544
|
+
background: var(--paper);
|
|
1545
|
+
padding: 16px;
|
|
1546
|
+
border: 1px solid var(--border);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/* ── Project Cards Grid ── */
|
|
1550
|
+
.projectCardsGrid {
|
|
1551
|
+
display: grid;
|
|
1552
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1553
|
+
gap: 16px;
|
|
1554
|
+
margin-bottom: 24px;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.projectCard {
|
|
1558
|
+
background: var(--paper);
|
|
1559
|
+
padding: 16px;
|
|
1560
|
+
border-left: 3px solid #4ade80;
|
|
1561
|
+
cursor: pointer;
|
|
1562
|
+
transition: box-shadow 0.2s ease;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
.projectCard:hover {
|
|
1566
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.projectCard.at_risk {
|
|
1570
|
+
border-left-color: #ff9900;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.projectCard.idle {
|
|
1574
|
+
border-left-color: #666;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/* ── Stream Item ── */
|
|
1578
|
+
.streamItem {
|
|
1579
|
+
padding: 8px;
|
|
1580
|
+
margin: 4px 0;
|
|
1581
|
+
border: 1px solid var(--border);
|
|
1582
|
+
cursor: pointer;
|
|
1583
|
+
transition: background 0.2s ease;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.streamItem:hover {
|
|
1587
|
+
background: var(--bg);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/* ── Alert Item ── */
|
|
1591
|
+
.alertItem {
|
|
1592
|
+
padding: 12px;
|
|
1593
|
+
margin: 8px 0;
|
|
1594
|
+
border-left: 3px solid #ef4444;
|
|
1595
|
+
background: rgba(239, 68, 68, 0.05);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1504
1598
|
* {
|
|
1505
1599
|
border-radius: 0 !important;
|
|
1506
1600
|
}
|
package/cli/web-ui.js
CHANGED
|
@@ -2130,6 +2130,172 @@
|
|
|
2130
2130
|
}
|
|
2131
2131
|
}
|
|
2132
2132
|
|
|
2133
|
+
async function refreshCompanionDash() {
|
|
2134
|
+
try {
|
|
2135
|
+
setPill('run', 'carregando dashboard...');
|
|
2136
|
+
const [pRes, sRes, aRes] = await Promise.all([
|
|
2137
|
+
api('/api/companion/projects-summary', { dir: dirOrDefault() }),
|
|
2138
|
+
api('/api/companion/streams-breakdown', { dir: dirOrDefault() }),
|
|
2139
|
+
api('/api/companion/alerts', { dir: dirOrDefault() })
|
|
2140
|
+
]);
|
|
2141
|
+
if (!pRes || !pRes.ok || !sRes || !sRes.ok || !aRes || !aRes.ok) {
|
|
2142
|
+
setPill('err', 'Falha ao carregar dashboard');
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
renderConsolidatedView(pRes.projects || []);
|
|
2146
|
+
renderProjectCards(pRes.projects || []);
|
|
2147
|
+
renderStreamBreakdown(sRes.items || []);
|
|
2148
|
+
renderAlerts(aRes.alerts || []);
|
|
2149
|
+
setPill('ok', 'dashboard pronto');
|
|
2150
|
+
} catch (e) {
|
|
2151
|
+
setPill('err', 'Erro ao carregar dashboard');
|
|
2152
|
+
console.error('refreshCompanionDash error:', e);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function renderConsolidatedView(projects) {
|
|
2157
|
+
const consolidated = $('companionConsolidated');
|
|
2158
|
+
if (!consolidated) return;
|
|
2159
|
+
let totalCompleted = 0, totalPending = 0, totalCritical = 0;
|
|
2160
|
+
for (const p of projects) {
|
|
2161
|
+
totalCompleted += p.completedTasks || 0;
|
|
2162
|
+
totalPending += p.pendingTasks || 0;
|
|
2163
|
+
totalCritical += (p.blockersBySeverity && p.blockersBySeverity.CRITICAL) || 0;
|
|
2164
|
+
}
|
|
2165
|
+
const avgVelocity = projects.length > 0 ? (projects.reduce((s, p) => s + (p.velocityThisWeek || 0), 0) / projects.length).toFixed(1) : '0';
|
|
2166
|
+
const staleProjects = projects.filter(p => p.status === 'IDLE').length;
|
|
2167
|
+
consolidated.innerHTML = `
|
|
2168
|
+
<div class="kpiCard">
|
|
2169
|
+
<div style="font-size:11px; color:var(--textSecondary); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Concluídas</div>
|
|
2170
|
+
<div style="font-size:20px; font-weight:700;">✅ ${totalCompleted}</div>
|
|
2171
|
+
</div>
|
|
2172
|
+
<div class="kpiCard">
|
|
2173
|
+
<div style="font-size:11px; color:var(--textSecondary); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Pendentes</div>
|
|
2174
|
+
<div style="font-size:20px; font-weight:700;">⏳ ${totalPending}</div>
|
|
2175
|
+
</div>
|
|
2176
|
+
<div class="kpiCard">
|
|
2177
|
+
<div style="font-size:11px; color:var(--textSecondary); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">CRITICAL</div>
|
|
2178
|
+
<div style="font-size:20px; font-weight:700; color:#ef4444;">🚧 ${totalCritical}</div>
|
|
2179
|
+
</div>
|
|
2180
|
+
<div class="kpiCard">
|
|
2181
|
+
<div style="font-size:11px; color:var(--textSecondary); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Velocity</div>
|
|
2182
|
+
<div style="font-size:20px; font-weight:700;">📈 ${avgVelocity}/dia</div>
|
|
2183
|
+
</div>
|
|
2184
|
+
`;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function renderProjectCards(projects) {
|
|
2188
|
+
const grid = $('companionProjects');
|
|
2189
|
+
if (!grid) return;
|
|
2190
|
+
const cards = projects.map(p => {
|
|
2191
|
+
const progressPct = p.totalTasks > 0 ? Math.round((p.completedTasks / p.totalTasks) * 100) : 0;
|
|
2192
|
+
const statusColor = p.status === 'ON_TRACK' ? '#10b981' : p.status === 'AT_RISK' ? '#ff9900' : '#666';
|
|
2193
|
+
const statusLabel = p.status === 'ON_TRACK' ? 'Em dia' : p.status === 'AT_RISK' ? 'Em risco' : 'Inativo';
|
|
2194
|
+
return `
|
|
2195
|
+
<div class="projectCard" style="border-left-color:${statusColor};">
|
|
2196
|
+
<div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:10px;">
|
|
2197
|
+
<div>
|
|
2198
|
+
<div style="font-weight:700; font-size:14px;">${escapeHtml(p.name)}</div>
|
|
2199
|
+
<div style="font-size:11px; color:var(--textSecondary); margin-top:2px;">${statusLabel}</div>
|
|
2200
|
+
</div>
|
|
2201
|
+
<span class="pill" style="background:${statusColor}22; border-color:${statusColor}55; color:${statusColor};">${progressPct}%</span>
|
|
2202
|
+
</div>
|
|
2203
|
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:10px; padding:8px; background:var(--bg); border-radius:0;">
|
|
2204
|
+
<div style="text-align:center;">
|
|
2205
|
+
<div style="font-size:10px; color:var(--textSecondary);">Concluídas</div>
|
|
2206
|
+
<div style="font-size:14px; font-weight:700;">✓ ${p.completedTasks}/${p.totalTasks}</div>
|
|
2207
|
+
</div>
|
|
2208
|
+
<div style="text-align:center;">
|
|
2209
|
+
<div style="font-size:10px; color:var(--textSecondary);">Blockers</div>
|
|
2210
|
+
<div style="font-size:14px; font-weight:700;">🚧 ${p.blockersCount}</div>
|
|
2211
|
+
</div>
|
|
2212
|
+
</div>
|
|
2213
|
+
<div style="font-size:10px; color:var(--textSecondary);">
|
|
2214
|
+
<div>📈 Velocity: ${p.velocityThisWeek.toFixed(1)}/dia</div>
|
|
2215
|
+
<div style="margin-top:2px;">⏱️ Última: ${escapeHtml(p.lastUpdateAgo)}</div>
|
|
2216
|
+
</div>
|
|
2217
|
+
</div>
|
|
2218
|
+
`;
|
|
2219
|
+
}).join('');
|
|
2220
|
+
grid.innerHTML = cards || '<div class="help">Nenhum projeto cadastrado.</div>';
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
function renderStreamBreakdown(data) {
|
|
2224
|
+
const el = $('streamBreakdown');
|
|
2225
|
+
if (!el) return;
|
|
2226
|
+
let html = '';
|
|
2227
|
+
for (const proj of data) {
|
|
2228
|
+
html += `<div style="margin-bottom:16px;">
|
|
2229
|
+
<div style="font-weight:700; margin-bottom:8px; color:var(--accent);">${escapeHtml(proj.projectName)}</div>`;
|
|
2230
|
+
for (const stream of proj.streams) {
|
|
2231
|
+
const pct = stream.totalTasks > 0 ? Math.round((stream.completedTasks / stream.totalTasks) * 100) : 0;
|
|
2232
|
+
const blockersLabel = stream.blockersCount > 0 ? `🚧 ${stream.blockersCount} ${stream.blockersHighestSeverity || ''}` : '✓';
|
|
2233
|
+
html += `<div class="streamItem">
|
|
2234
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
2235
|
+
<div>
|
|
2236
|
+
<div style="font-size:12px;">${escapeHtml(stream.streamName)}</div>
|
|
2237
|
+
<div style="font-size:10px; color:var(--textSecondary); margin-top:2px;">✓ ${stream.completedTasks}/${stream.totalTasks} (${pct}%) | ${blockersLabel}</div>
|
|
2238
|
+
</div>
|
|
2239
|
+
</div>
|
|
2240
|
+
</div>`;
|
|
2241
|
+
}
|
|
2242
|
+
html += `</div>`;
|
|
2243
|
+
}
|
|
2244
|
+
el.innerHTML = html || '<div class="help">Nenhuma frente cadastrada.</div>';
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function renderAlerts(alerts) {
|
|
2248
|
+
const alertsZone = $('companionAlerts');
|
|
2249
|
+
const alertsList = $('alertsList');
|
|
2250
|
+
if (!alertsList) return;
|
|
2251
|
+
if (!alerts || alerts.length === 0) {
|
|
2252
|
+
alertsList.innerHTML = '<div class="help">✓ Nenhum alerta no momento!</div>';
|
|
2253
|
+
if (alertsZone) alertsZone.style.display = 'none';
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const items = alerts.map(a => {
|
|
2257
|
+
const sevColor = a.severity === 'CRITICAL' ? '#ef4444' : a.severity === 'HIGH' ? '#ff9900' : '#eab308';
|
|
2258
|
+
const sevLabel = a.severity === 'CRITICAL' ? '🔴 CRÍTICO' : a.severity === 'HIGH' ? '🟠 ALTO' : '🟡 MÉDIO';
|
|
2259
|
+
return `<div class="alertItem" style="border-left-color:${sevColor}; background:${sevColor}11;">
|
|
2260
|
+
<div style="display:flex; justify-content:space-between; gap:10px; align-items:start;">
|
|
2261
|
+
<div style="min-width:0; flex:1;">
|
|
2262
|
+
<div style="font-weight:700; color:${sevColor};">${sevLabel}</div>
|
|
2263
|
+
<div style="font-size:12px; margin-top:4px;">${escapeHtml(a.message)}</div>
|
|
2264
|
+
${a.projectSlug ? `<div style="font-size:10px; color:var(--textSecondary); margin-top:4px;">Projeto: ${escapeHtml(a.projectSlug)}</div>` : ''}
|
|
2265
|
+
</div>
|
|
2266
|
+
</div>
|
|
2267
|
+
</div>`;
|
|
2268
|
+
}).join('');
|
|
2269
|
+
alertsList.innerHTML = items;
|
|
2270
|
+
if (alertsZone) alertsZone.style.display = 'block';
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
function filterCompanionView(filter) {
|
|
2274
|
+
const tabs = document.querySelectorAll('.companionTabs .tab');
|
|
2275
|
+
tabs.forEach(t => t.classList.remove('active'));
|
|
2276
|
+
document.querySelector(`.companionTabs .tab[data-filter="${filter}"]`)?.classList.add('active');
|
|
2277
|
+
const consolidated = $('companionConsolidated');
|
|
2278
|
+
const projects = $('companionProjects');
|
|
2279
|
+
const streams = $('streamBreakdown').parentElement.parentElement;
|
|
2280
|
+
const alerts = $('companionAlerts');
|
|
2281
|
+
if (filter === 'all') {
|
|
2282
|
+
if (consolidated) consolidated.style.display = 'grid';
|
|
2283
|
+
if (projects) projects.style.display = 'grid';
|
|
2284
|
+
if (streams) streams.style.display = 'block';
|
|
2285
|
+
if (alerts) alerts.style.display = 'none';
|
|
2286
|
+
} else if (filter === 'alerts') {
|
|
2287
|
+
if (consolidated) consolidated.style.display = 'none';
|
|
2288
|
+
if (projects) projects.style.display = 'none';
|
|
2289
|
+
if (streams) streams.style.display = 'none';
|
|
2290
|
+
if (alerts) alerts.style.display = 'block';
|
|
2291
|
+
} else if (filter === 'risk') {
|
|
2292
|
+
if (consolidated) consolidated.style.display = 'grid';
|
|
2293
|
+
if (projects) projects.style.display = 'grid';
|
|
2294
|
+
if (streams) streams.style.display = 'block';
|
|
2295
|
+
if (alerts) alerts.style.display = 'none';
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2133
2299
|
async function doHealth() {
|
|
2134
2300
|
try {
|
|
2135
2301
|
saveLocal();
|
|
@@ -2723,6 +2889,8 @@
|
|
|
2723
2889
|
window.refreshExecutiveSummary = refreshExecutiveSummary;
|
|
2724
2890
|
window.refreshAnomalies = refreshAnomalies;
|
|
2725
2891
|
window.refreshRiskRadar = refreshRiskRadar;
|
|
2892
|
+
window.refreshCompanionDash = refreshCompanionDash;
|
|
2893
|
+
window.filterCompanionView = filterCompanionView;
|
|
2726
2894
|
window.copyOut = copyOut;
|
|
2727
2895
|
window.copyPath = copyPath;
|
|
2728
2896
|
window.openSelected = openSelected;
|
package/cli/web.js
CHANGED
|
@@ -1797,97 +1797,42 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1797
1797
|
<section class="reportsHeader">
|
|
1798
1798
|
<div>
|
|
1799
1799
|
<div class="reportsTitle">Scrum Master Companion</div>
|
|
1800
|
-
<div class="reportsSubtitle">
|
|
1800
|
+
<div class="reportsSubtitle">Sprint Health Dashboard — Visão consolidada de seus projetos</div>
|
|
1801
1801
|
</div>
|
|
1802
1802
|
<div class="reportsActions">
|
|
1803
|
-
<button class="btn small" type="button" onclick="
|
|
1803
|
+
<button class="btn small" type="button" onclick="refreshCompanionDash()">↻ Atualizar</button>
|
|
1804
1804
|
</div>
|
|
1805
1805
|
</section>
|
|
1806
1806
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
<!-- Left Column / Block 1 -->
|
|
1815
|
-
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
1816
|
-
<section class="panel">
|
|
1817
|
-
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1818
|
-
<b>Resumo Executivo</b>
|
|
1819
|
-
<button class="btn small" type="button" onclick="refreshExecutiveSummary()">↻</button>
|
|
1820
|
-
</div>
|
|
1821
|
-
<div class="panelBody">
|
|
1822
|
-
<div id="executiveSummary" class="help"></div>
|
|
1823
|
-
</div>
|
|
1824
|
-
</section>
|
|
1825
|
-
|
|
1826
|
-
<section class="panel">
|
|
1827
|
-
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1828
|
-
<b>Radar de Risco</b>
|
|
1829
|
-
<button class="btn small" type="button" onclick="refreshRiskRadar()">↻</button>
|
|
1830
|
-
</div>
|
|
1831
|
-
<div class="panelBody">
|
|
1832
|
-
<div id="riskRadarBox"></div>
|
|
1833
|
-
</div>
|
|
1834
|
-
</section>
|
|
1835
|
-
|
|
1836
|
-
<section class="panel">
|
|
1837
|
-
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1838
|
-
<b>Task Heatmap</b>
|
|
1839
|
-
</div>
|
|
1840
|
-
<div class="panelBody">
|
|
1841
|
-
<div id="heatmapGrid"></div>
|
|
1842
|
-
</div>
|
|
1843
|
-
</section>
|
|
1844
|
-
</div>
|
|
1845
|
-
|
|
1846
|
-
<!-- Right Column / Block 2 -->
|
|
1847
|
-
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
1848
|
-
<section class="panel">
|
|
1849
|
-
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1850
|
-
<b style="color: var(--accent);">Saúde & Checklists</b>
|
|
1851
|
-
</div>
|
|
1852
|
-
<div class="panelBody" style="padding: 0;">
|
|
1853
|
-
<section class="reportsGrid" id="healthChecklist" style="gap: 0; border: none; box-shadow: none;"></section>
|
|
1854
|
-
</div>
|
|
1855
|
-
</section>
|
|
1807
|
+
<!-- TAB FILTERS -->
|
|
1808
|
+
<div class="companionTabs">
|
|
1809
|
+
<button class="tab active" data-filter="all" onclick="filterCompanionView('all')">○ Todos</button>
|
|
1810
|
+
<button class="tab" data-filter="alerts" onclick="filterCompanionView('alerts')">⚠️ Alertas</button>
|
|
1811
|
+
<button class="tab" data-filter="risk" onclick="filterCompanionView('risk')">🔴 Risco Alto</button>
|
|
1812
|
+
</div>
|
|
1856
1813
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1860
|
-
<b>Qualidade de Log</b>
|
|
1861
|
-
<button class="btn small" type="button" onclick="refreshQualityScore()">↻</button>
|
|
1862
|
-
</div>
|
|
1863
|
-
<div class="panelBody">
|
|
1864
|
-
<div id="qualityScoreCard"></div>
|
|
1865
|
-
</div>
|
|
1866
|
-
</section>
|
|
1814
|
+
<!-- CONSOLIDATED VIEW -->
|
|
1815
|
+
<div id="companionConsolidated" class="consolidatedView"></div>
|
|
1867
1816
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
<b>Anomalias</b>
|
|
1871
|
-
<button class="btn small" type="button" onclick="refreshAnomalies()">↻</button>
|
|
1872
|
-
</div>
|
|
1873
|
-
<div class="panelBody">
|
|
1874
|
-
<div id="anomaliesBox"></div>
|
|
1875
|
-
</div>
|
|
1876
|
-
</section>
|
|
1877
|
-
</div>
|
|
1878
|
-
</div>
|
|
1879
|
-
|
|
1880
|
-
<!-- Full Width Span -->
|
|
1881
|
-
<div style="grid-column: 1 / -1; display: flex; flex-direction: column; gap: 16px;">
|
|
1882
|
-
<section class="panel">
|
|
1883
|
-
<div class="panelHead"><b>Incident Radar</b></div>
|
|
1884
|
-
<div class="panelBody">
|
|
1885
|
-
<div id="incidentsBox" class="log md" style="font-family: var(--sans);"></div>
|
|
1886
|
-
</div>
|
|
1887
|
-
</section>
|
|
1817
|
+
<!-- PROJECT CARDS GRID -->
|
|
1818
|
+
<div id="companionProjects" class="projectCardsGrid"></div>
|
|
1888
1819
|
|
|
1820
|
+
<!-- STREAM BREAKDOWN -->
|
|
1821
|
+
<section class="panel" style="margin-top: 24px;">
|
|
1822
|
+
<div class="panelHead"><b>Distribuição por Frente/Stream</b></div>
|
|
1823
|
+
<div class="panelBody">
|
|
1824
|
+
<div id="streamBreakdown" class="help">Carregando streams...</div>
|
|
1889
1825
|
</div>
|
|
1826
|
+
</section>
|
|
1890
1827
|
|
|
1828
|
+
<!-- ALERTS ZONE -->
|
|
1829
|
+
<div id="companionAlerts" style="display: none; margin-top: 24px;">
|
|
1830
|
+
<section class="panel">
|
|
1831
|
+
<div class="panelHead"><b>⚠️ Alertas Prioritários</b></div>
|
|
1832
|
+
<div class="panelBody">
|
|
1833
|
+
<div id="alertsList"></div>
|
|
1834
|
+
</div>
|
|
1835
|
+
</section>
|
|
1891
1836
|
</div>
|
|
1892
1837
|
</div>
|
|
1893
1838
|
</main>
|
|
@@ -2053,6 +1998,18 @@ function truncateText(text, maxLen) {
|
|
|
2053
1998
|
return str.slice(0, Math.max(0, maxLen - 3)).trimEnd() + '...';
|
|
2054
1999
|
}
|
|
2055
2000
|
|
|
2001
|
+
function formatAgo(timestampMs) {
|
|
2002
|
+
const ms = typeof timestampMs === 'number' ? timestampMs : (typeof timestampMs === 'string' ? Date.parse(timestampMs) : 0);
|
|
2003
|
+
if (!Number.isFinite(ms)) return 'unknown';
|
|
2004
|
+
const now = Date.now();
|
|
2005
|
+
const diffMs = now - ms;
|
|
2006
|
+
if (diffMs < 60000) return 'agora';
|
|
2007
|
+
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m`;
|
|
2008
|
+
if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h`;
|
|
2009
|
+
if (diffMs < 604800000) return `${Math.floor(diffMs / 86400000)}d`;
|
|
2010
|
+
return `${Math.floor(diffMs / 604800000)}w`;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2056
2013
|
function getTimelineItems(workspaceDir) {
|
|
2057
2014
|
const items = [];
|
|
2058
2015
|
const dailyDir = path.join(workspaceDir, 'logs', 'daily');
|
|
@@ -3709,6 +3666,202 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3709
3666
|
return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
|
|
3710
3667
|
}
|
|
3711
3668
|
|
|
3669
|
+
if (req.url === '/api/companion/projects-summary') {
|
|
3670
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3671
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', projects: [] });
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
const now = Date.now();
|
|
3675
|
+
const projects = {};
|
|
3676
|
+
|
|
3677
|
+
// Aggregate tasks by projectSlug
|
|
3678
|
+
const tasksRaw = dl.db.prepare('SELECT * FROM tasks WHERE status IN ("PENDING", "COMPLETED")').all();
|
|
3679
|
+
const blockersRaw = dl.db.prepare('SELECT * FROM blockers WHERE status IN ("OPEN", "MITIGATING")').all();
|
|
3680
|
+
|
|
3681
|
+
// Build project summary
|
|
3682
|
+
for (const t of tasksRaw) {
|
|
3683
|
+
const slug = String(t.project_slug || 'unassigned').trim();
|
|
3684
|
+
if (!projects[slug]) {
|
|
3685
|
+
projects[slug] = { slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0, streams: new Set(), blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }, oldestBlockerDays: null, lastUpdateMs: null };
|
|
3686
|
+
}
|
|
3687
|
+
projects[slug].totalTasks++;
|
|
3688
|
+
if (String(t.status).toUpperCase() === 'COMPLETED') projects[slug].completedTasks++;
|
|
3689
|
+
else projects[slug].pendingTasks++;
|
|
3690
|
+
if (t.completed_at) projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, Date.parse(t.completed_at));
|
|
3691
|
+
if (t.created_at) projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, Date.parse(t.created_at));
|
|
3692
|
+
const meta = t.metadata ? JSON.parse(String(t.metadata || '{}')) : {};
|
|
3693
|
+
if (meta.streamSlug) projects[slug].streams.add(String(meta.streamSlug));
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// Add blockers to projects
|
|
3697
|
+
for (const b of blockersRaw) {
|
|
3698
|
+
const slug = String(b.project_slug || 'unassigned').trim();
|
|
3699
|
+
if (!projects[slug]) {
|
|
3700
|
+
projects[slug] = { slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0, streams: new Set(), blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }, oldestBlockerDays: null, lastUpdateMs: null };
|
|
3701
|
+
}
|
|
3702
|
+
projects[slug].blockersCount++;
|
|
3703
|
+
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
3704
|
+
if (projects[slug].blockersBySeverity[sev] !== undefined) projects[slug].blockersBySeverity[sev]++;
|
|
3705
|
+
const createdMs = b.created_at ? Date.parse(b.created_at) : null;
|
|
3706
|
+
if (createdMs) {
|
|
3707
|
+
projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, createdMs);
|
|
3708
|
+
const ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
|
|
3709
|
+
if (projects[slug].oldestBlockerDays === null || ageDays > projects[slug].oldestBlockerDays) {
|
|
3710
|
+
projects[slug].oldestBlockerDays = ageDays;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
// Convert to array and calculate status + velocity
|
|
3716
|
+
const projectsArray = Object.values(projects).map(p => {
|
|
3717
|
+
const completedThisWeek = dl.db.prepare('SELECT count(*) as count FROM tasks WHERE project_slug = ? AND status = "COMPLETED" AND completed_at > ?').get(p.slug, new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString());
|
|
3718
|
+
const velocityThisWeek = completedThisWeek ? (completedThisWeek.count / 7).toFixed(1) : '0';
|
|
3719
|
+
const lastUpdateAgo = p.lastUpdateMs ? formatAgo(p.lastUpdateMs) : 'nunca';
|
|
3720
|
+
const hasOldBlockers = p.oldestBlockerDays && p.oldestBlockerDays > 7;
|
|
3721
|
+
const hasHighSeverity = p.blockersBySeverity.CRITICAL > 0 || p.blockersBySeverity.HIGH > 0;
|
|
3722
|
+
const isIdle = p.lastUpdateMs && (now - p.lastUpdateMs) > 7 * 24 * 60 * 60 * 1000;
|
|
3723
|
+
const status = isIdle ? 'IDLE' : (hasHighSeverity || hasOldBlockers ? 'AT_RISK' : 'ON_TRACK');
|
|
3724
|
+
return {
|
|
3725
|
+
slug: p.slug,
|
|
3726
|
+
name: p.slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
3727
|
+
totalTasks: p.totalTasks,
|
|
3728
|
+
completedTasks: p.completedTasks,
|
|
3729
|
+
pendingTasks: p.pendingTasks,
|
|
3730
|
+
blockersCount: p.blockersCount,
|
|
3731
|
+
blockersBySeverity: p.blockersBySeverity,
|
|
3732
|
+
oldestBlockerDays: p.oldestBlockerDays,
|
|
3733
|
+
velocityThisWeek: parseFloat(velocityThisWeek),
|
|
3734
|
+
lastUpdateAgo: lastUpdateAgo,
|
|
3735
|
+
status: status,
|
|
3736
|
+
streams: Array.from(p.streams)
|
|
3737
|
+
};
|
|
3738
|
+
});
|
|
3739
|
+
|
|
3740
|
+
return safeJson(res, 200, { ok: true, projects: projectsArray });
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
if (req.url === '/api/companion/streams-breakdown') {
|
|
3744
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3745
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', items: [] });
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
const tasks = dl.db.prepare('SELECT * FROM tasks').all();
|
|
3749
|
+
const blockers = dl.db.prepare('SELECT * FROM blockers').all();
|
|
3750
|
+
|
|
3751
|
+
const breakdown = {};
|
|
3752
|
+
for (const t of tasks) {
|
|
3753
|
+
const projectSlug = String(t.project_slug || 'unassigned');
|
|
3754
|
+
if (!breakdown[projectSlug]) breakdown[projectSlug] = {};
|
|
3755
|
+
const meta = t.metadata ? JSON.parse(String(t.metadata || '{}')) : {};
|
|
3756
|
+
const streamName = String(meta.streamSlug || 'Default');
|
|
3757
|
+
if (!breakdown[projectSlug][streamName]) {
|
|
3758
|
+
breakdown[projectSlug][streamName] = { totalTasks: 0, completedTasks: 0, pendingTasks: 0, blockersCount: 0, blockersHighestSeverity: null };
|
|
3759
|
+
}
|
|
3760
|
+
breakdown[projectSlug][streamName].totalTasks++;
|
|
3761
|
+
if (String(t.status).toUpperCase() === 'COMPLETED') breakdown[projectSlug][streamName].completedTasks++;
|
|
3762
|
+
else breakdown[projectSlug][streamName].pendingTasks++;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
for (const b of blockers) {
|
|
3766
|
+
if (String(b.status).toUpperCase() !== 'OPEN' && String(b.status).toUpperCase() !== 'MITIGATING') continue;
|
|
3767
|
+
const projectSlug = String(b.project_slug || 'unassigned');
|
|
3768
|
+
if (!breakdown[projectSlug]) breakdown[projectSlug] = {};
|
|
3769
|
+
const meta = b.metadata ? JSON.parse(String(b.metadata || '{}')) : {};
|
|
3770
|
+
const streamName = String(meta.streamSlug || 'Default');
|
|
3771
|
+
if (!breakdown[projectSlug][streamName]) {
|
|
3772
|
+
breakdown[projectSlug][streamName] = { totalTasks: 0, completedTasks: 0, pendingTasks: 0, blockersCount: 0, blockersHighestSeverity: null };
|
|
3773
|
+
}
|
|
3774
|
+
breakdown[projectSlug][streamName].blockersCount++;
|
|
3775
|
+
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
3776
|
+
const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
3777
|
+
const sevVal = sevOrder[sev] !== undefined ? sevOrder[sev] : 999;
|
|
3778
|
+
const currentVal = breakdown[projectSlug][streamName].blockersHighestSeverity ? sevOrder[breakdown[projectSlug][streamName].blockersHighestSeverity] : 999;
|
|
3779
|
+
if (sevVal < currentVal) breakdown[projectSlug][streamName].blockersHighestSeverity = sev;
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
const items = Object.entries(breakdown).map(([projectSlug, streams]) => ({
|
|
3783
|
+
projectSlug,
|
|
3784
|
+
projectName: projectSlug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
3785
|
+
streams: Object.entries(streams).map(([streamName, data]) => ({ streamName, ...data }))
|
|
3786
|
+
}));
|
|
3787
|
+
|
|
3788
|
+
return safeJson(res, 200, { ok: true, items });
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
if (req.url === '/api/companion/alerts') {
|
|
3792
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3793
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', alerts: [] });
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
const now = Date.now();
|
|
3797
|
+
const alerts = [];
|
|
3798
|
+
|
|
3799
|
+
const blockers = dl.db.prepare('SELECT * FROM blockers WHERE status IN ("OPEN", "MITIGATING")').all();
|
|
3800
|
+
const tasks = dl.db.prepare('SELECT * FROM tasks WHERE status = "PENDING"').all();
|
|
3801
|
+
|
|
3802
|
+
// Old blockers
|
|
3803
|
+
for (const b of blockers) {
|
|
3804
|
+
const createdMs = b.created_at ? Date.parse(b.created_at) : null;
|
|
3805
|
+
if (createdMs) {
|
|
3806
|
+
const ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
|
|
3807
|
+
if (ageDays > 3) {
|
|
3808
|
+
const sev = String(b.severity || '').toUpperCase();
|
|
3809
|
+
alerts.push({
|
|
3810
|
+
severity: sev === 'CRITICAL' ? 'CRITICAL' : sev === 'HIGH' ? 'HIGH' : 'MEDIUM',
|
|
3811
|
+
type: 'old_blocker',
|
|
3812
|
+
projectSlug: String(b.project_slug || ''),
|
|
3813
|
+
streamName: 'blocker: ' + (b.title || ''),
|
|
3814
|
+
message: `Blocker aberto há ${ageDays} dias: ${b.title || 'Sem titulo'}`,
|
|
3815
|
+
ageDays
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// Stale projects (7+ days without update)
|
|
3822
|
+
const projects = {};
|
|
3823
|
+
for (const t of tasks) {
|
|
3824
|
+
const slug = String(t.project_slug || '');
|
|
3825
|
+
if (slug) {
|
|
3826
|
+
const createdMs = t.created_at ? Date.parse(t.created_at) : null;
|
|
3827
|
+
if (createdMs && (!projects[slug] || createdMs > projects[slug])) projects[slug] = createdMs;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
for (const b of blockers) {
|
|
3831
|
+
const slug = String(b.project_slug || '');
|
|
3832
|
+
if (slug) {
|
|
3833
|
+
const createdMs = b.created_at ? Date.parse(b.created_at) : null;
|
|
3834
|
+
if (createdMs && (!projects[slug] || createdMs > projects[slug])) projects[slug] = createdMs;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
for (const [slug, lastMs] of Object.entries(projects)) {
|
|
3838
|
+
if (lastMs) {
|
|
3839
|
+
const ageDays = Math.floor((now - lastMs) / (24 * 60 * 60 * 1000));
|
|
3840
|
+
if (ageDays > 7) {
|
|
3841
|
+
alerts.push({
|
|
3842
|
+
severity: ageDays > 14 ? 'HIGH' : 'MEDIUM',
|
|
3843
|
+
type: 'stale_project',
|
|
3844
|
+
projectSlug: slug,
|
|
3845
|
+
streamName: '',
|
|
3846
|
+
message: `Projeto ${slug} sem updates há ${ageDays} dias`,
|
|
3847
|
+
ageDays
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// Sort by severity + age
|
|
3854
|
+
alerts.sort((a, b) => {
|
|
3855
|
+
const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2 };
|
|
3856
|
+
const aVal = sevOrder[a.severity] || 99;
|
|
3857
|
+
const bVal = sevOrder[b.severity] || 99;
|
|
3858
|
+
if (aVal !== bVal) return aVal - bVal;
|
|
3859
|
+
return (b.ageDays || 0) - (a.ageDays || 0);
|
|
3860
|
+
});
|
|
3861
|
+
|
|
3862
|
+
return safeJson(res, 200, { ok: true, alerts: alerts.slice(0, 10) });
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3712
3865
|
// BUG-12: duplicate handlers for /api/incidents/resolve, /api/incidents,
|
|
3713
3866
|
// and /api/tasks/heatmap were removed here — originals remain earlier in the file.
|
|
3714
3867
|
|
package/package.json
CHANGED