@cccarv82/freya 2.13.4 → 2.14.1
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 +110 -1
- package/cli/web-ui.js +194 -21
- package/cli/web.js +341 -77
- package/package.json +2 -2
package/cli/web-ui.css
CHANGED
|
@@ -371,7 +371,6 @@ body {
|
|
|
371
371
|
align-items: center;
|
|
372
372
|
padding: 16px 20px 10px;
|
|
373
373
|
background: transparent;
|
|
374
|
-
flex-shrink: 0;
|
|
375
374
|
}
|
|
376
375
|
|
|
377
376
|
.brandLine {
|
|
@@ -593,10 +592,15 @@ body {
|
|
|
593
592
|
gap: 18px;
|
|
594
593
|
flex: 1;
|
|
595
594
|
}
|
|
595
|
+
|
|
596
596
|
.centerBody > * {
|
|
597
597
|
flex-shrink: 0;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
.topbar {
|
|
601
|
+
flex-shrink: 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
600
604
|
.promptShell {
|
|
601
605
|
display: flex;
|
|
602
606
|
justify-content: center;
|
|
@@ -1501,6 +1505,111 @@ textarea:focus {
|
|
|
1501
1505
|
animation: fadeIn 0.2s ease-out;
|
|
1502
1506
|
}
|
|
1503
1507
|
|
|
1508
|
+
/* ── Companion Dashboard Styles ── */
|
|
1509
|
+
.companionTabs {
|
|
1510
|
+
display: flex;
|
|
1511
|
+
gap: 8px;
|
|
1512
|
+
margin: 16px 0;
|
|
1513
|
+
border-bottom: 1px solid var(--border);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.companionTabs .tab {
|
|
1517
|
+
padding: 8px 12px;
|
|
1518
|
+
background: transparent;
|
|
1519
|
+
border: none;
|
|
1520
|
+
border-bottom: 2px solid transparent;
|
|
1521
|
+
cursor: pointer;
|
|
1522
|
+
font-size: 12px;
|
|
1523
|
+
color: var(--textMuted);
|
|
1524
|
+
transition: color 0.2s, border-color 0.2s;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.companionTabs .tab:hover {
|
|
1528
|
+
color: var(--text);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
.companionTabs .tab.active {
|
|
1532
|
+
color: var(--accent);
|
|
1533
|
+
border-bottom-color: var(--accent);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
.consolidatedView {
|
|
1537
|
+
display: grid;
|
|
1538
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1539
|
+
gap: 16px;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
.kpiCard {
|
|
1543
|
+
background: var(--paper);
|
|
1544
|
+
padding: 16px;
|
|
1545
|
+
border: 1px solid var(--border);
|
|
1546
|
+
display: flex;
|
|
1547
|
+
flex-direction: column;
|
|
1548
|
+
gap: 4px;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.projectCardsGrid {
|
|
1552
|
+
display: grid;
|
|
1553
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1554
|
+
gap: 16px;
|
|
1555
|
+
margin: 16px 0;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
@media (max-width: 1200px) {
|
|
1559
|
+
.projectCardsGrid {
|
|
1560
|
+
grid-template-columns: 1fr;
|
|
1561
|
+
}
|
|
1562
|
+
.consolidatedView {
|
|
1563
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.projectCard {
|
|
1568
|
+
background: var(--paper);
|
|
1569
|
+
padding: 16px;
|
|
1570
|
+
border-left: 3px solid #4ade80;
|
|
1571
|
+
border: 1px solid var(--border);
|
|
1572
|
+
border-left: 3px solid #4ade80;
|
|
1573
|
+
cursor: pointer;
|
|
1574
|
+
transition: background 0.2s, border-color 0.2s;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
.projectCard:hover {
|
|
1578
|
+
background: rgba(255, 255, 255, 0.02);
|
|
1579
|
+
border-color: var(--accent);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.projectCard.at_risk {
|
|
1583
|
+
border-left-color: #ff9900;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.projectCard.idle {
|
|
1587
|
+
border-left-color: #666;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.streamItem {
|
|
1591
|
+
padding: 12px;
|
|
1592
|
+
margin: 4px 0;
|
|
1593
|
+
border: 1px solid var(--border);
|
|
1594
|
+
background: transparent;
|
|
1595
|
+
transition: background 0.2s;
|
|
1596
|
+
font-size: 11px;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.streamItem:hover {
|
|
1600
|
+
background: rgba(255, 255, 255, 0.01);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.alertItem {
|
|
1604
|
+
padding: 12px;
|
|
1605
|
+
margin: 8px 0;
|
|
1606
|
+
border-left: 3px solid #ef4444;
|
|
1607
|
+
background: rgba(239, 68, 68, 0.05);
|
|
1608
|
+
border: 1px solid var(--border);
|
|
1609
|
+
border-left: 3px solid #ef4444;
|
|
1610
|
+
font-size: 11px;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1504
1613
|
* {
|
|
1505
1614
|
border-radius: 0 !important;
|
|
1506
1615
|
}
|
package/cli/web-ui.js
CHANGED
|
@@ -139,10 +139,9 @@
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
const li = line.match(/^[ \t]*[-*][ \t]+(.*)$/);
|
|
142
|
-
|
|
143
|
-
if (li || oli) {
|
|
142
|
+
if (li) {
|
|
144
143
|
if (!inList) { html += '<ul class="md-ul">'; inList = true; }
|
|
145
|
-
const content = inlineFormat(
|
|
144
|
+
const content = inlineFormat(li[1]);
|
|
146
145
|
html += '<li>' + content + '</li>';
|
|
147
146
|
continue;
|
|
148
147
|
}
|
|
@@ -193,37 +192,42 @@
|
|
|
193
192
|
|
|
194
193
|
// --- Strategy 2: regex fallback for truncated/malformed JSON ---
|
|
195
194
|
var lines = [];
|
|
195
|
+
var num = 0;
|
|
196
196
|
|
|
197
197
|
// Match append_daily_log / appenddailylog actions
|
|
198
198
|
var logRe = /"type"\s*:\s*"append_?daily_?log"\s*,\s*"text"\s*:\s*"([^"]{1,300})/gi;
|
|
199
199
|
var m;
|
|
200
200
|
while ((m = logRe.exec(text)) !== null) {
|
|
201
|
+
num++;
|
|
201
202
|
var t = m[1].slice(0, 140);
|
|
202
|
-
lines.push('
|
|
203
|
+
lines.push(num + '. \u{1F4DD} **Registrar no log:** ' + t + (m[1].length > 140 ? '...' : ''));
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
// Match create_task actions
|
|
206
207
|
var taskRe = /"type"\s*:\s*"create_?task"\s*,\s*"description"\s*:\s*"([^"]{1,200})/gi;
|
|
207
208
|
while ((m = taskRe.exec(text)) !== null) {
|
|
209
|
+
num++;
|
|
208
210
|
var desc = m[1].slice(0, 120);
|
|
209
211
|
var priMatch = text.slice(m.index, m.index + 400).match(/"priority"\s*:\s*"(\w+)"/i);
|
|
210
212
|
var pri = priMatch ? ' (prioridade: **' + priMatch[1].toUpperCase() + '**)' : '';
|
|
211
|
-
lines.push('
|
|
213
|
+
lines.push(num + '. \u2705 **Criar tarefa:** ' + desc + pri);
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
// Match create_blocker actions
|
|
215
217
|
var blockerRe = /"type"\s*:\s*"create_?blocker"\s*,\s*"title"\s*:\s*"([^"]{1,200})/gi;
|
|
216
218
|
while ((m = blockerRe.exec(text)) !== null) {
|
|
219
|
+
num++;
|
|
217
220
|
var title = m[1].slice(0, 120);
|
|
218
221
|
var sevMatch = text.slice(m.index, m.index + 400).match(/"severity"\s*:\s*"(\w+)"/i);
|
|
219
222
|
var sev = sevMatch ? ' (severidade: **' + sevMatch[1].toUpperCase() + '**)' : '';
|
|
220
|
-
lines.push('
|
|
223
|
+
lines.push(num + '. \u{1F6A7} **Registrar blocker:** ' + title + sev);
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
// Match suggest_report actions
|
|
224
227
|
var repRe = /"type"\s*:\s*"suggest_?report"\s*,\s*"name"\s*:\s*"([^"]+)"/gi;
|
|
225
228
|
while ((m = repRe.exec(text)) !== null) {
|
|
226
|
-
|
|
229
|
+
num++;
|
|
230
|
+
lines.push(num + '. \u{1F4CA} **Sugerir relatorio:** ' + m[1]);
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
return lines.length > 0 ? lines.join('\n') : null;
|
|
@@ -236,32 +240,33 @@
|
|
|
236
240
|
oraclequery: '\u{1F50D}'
|
|
237
241
|
};
|
|
238
242
|
|
|
239
|
-
var lines = actions.map(function(a) {
|
|
243
|
+
var lines = actions.map(function(a, i) {
|
|
240
244
|
var type = String(a.type || '').trim().toLowerCase().replace(/_/g, '');
|
|
241
245
|
var icon = icons[type] || '\u2022';
|
|
246
|
+
var num = i + 1;
|
|
242
247
|
|
|
243
248
|
if (type === 'appenddailylog') {
|
|
244
249
|
var t = String(a.text || '').slice(0, 140);
|
|
245
|
-
return '
|
|
250
|
+
return num + '. ' + icon + ' **Registrar no log:** ' + t + (String(a.text || '').length > 140 ? '...' : '');
|
|
246
251
|
}
|
|
247
252
|
if (type === 'createtask') {
|
|
248
253
|
var desc = String(a.description || '').slice(0, 120);
|
|
249
254
|
var pri = a.priority ? ' (prioridade: **' + String(a.priority).toUpperCase() + '**)' : '';
|
|
250
255
|
var cat = a.category ? ' [' + a.category + ']' : '';
|
|
251
|
-
return '
|
|
256
|
+
return num + '. ' + icon + ' **Criar tarefa:** ' + desc + pri + cat;
|
|
252
257
|
}
|
|
253
258
|
if (type === 'createblocker') {
|
|
254
259
|
var title = String(a.title || a.description || '').slice(0, 120);
|
|
255
260
|
var sev = a.severity ? ' (severidade: **' + String(a.severity).toUpperCase() + '**)' : '';
|
|
256
|
-
return '
|
|
261
|
+
return num + '. ' + icon + ' **Registrar blocker:** ' + title + sev;
|
|
257
262
|
}
|
|
258
263
|
if (type === 'suggestreport') {
|
|
259
|
-
return '
|
|
264
|
+
return num + '. ' + icon + ' **Sugerir relatorio:** ' + String(a.name || a.reportType || '');
|
|
260
265
|
}
|
|
261
266
|
if (type === 'oraclequery') {
|
|
262
|
-
return '
|
|
267
|
+
return num + '. ' + icon + ' **Consultar oracle:** ' + String(a.query || '').slice(0, 120);
|
|
263
268
|
}
|
|
264
|
-
return '
|
|
269
|
+
return num + '. \u2022 **' + String(a.type || 'acao') + '**';
|
|
265
270
|
});
|
|
266
271
|
|
|
267
272
|
return lines.join('\n');
|
|
@@ -2130,6 +2135,179 @@
|
|
|
2130
2135
|
}
|
|
2131
2136
|
}
|
|
2132
2137
|
|
|
2138
|
+
// New Companion Dashboard functions
|
|
2139
|
+
async function refreshCompanionDash() {
|
|
2140
|
+
const filter = document.querySelector('.companionTabs .tab.active')?.dataset?.filter || 'all';
|
|
2141
|
+
try {
|
|
2142
|
+
const [prj, brk, alts] = await Promise.all([
|
|
2143
|
+
api('/api/companion/projects-summary', { dir: dirOrDefault() }),
|
|
2144
|
+
api('/api/companion/streams-breakdown', { dir: dirOrDefault() }),
|
|
2145
|
+
api('/api/companion/alerts', { dir: dirOrDefault() })
|
|
2146
|
+
]);
|
|
2147
|
+
|
|
2148
|
+
const projects = (prj && prj.projects) || [];
|
|
2149
|
+
const breakdown = (brk && brk.breakdown) || [];
|
|
2150
|
+
const alerts = (alts && alts.alerts) || [];
|
|
2151
|
+
|
|
2152
|
+
// Show/hide sections based on filter
|
|
2153
|
+
$('consolidatedViewBox').style.display = filter === 'all' ? 'block' : 'none';
|
|
2154
|
+
$('projectCardsBox').style.display = filter === 'all' ? 'block' : 'none';
|
|
2155
|
+
$('streamBreakdownBox').style.display = filter === 'all' ? 'block' : 'none';
|
|
2156
|
+
$('alertsViewBox').style.display = filter === 'alerts' || filter === 'risk' ? 'block' : 'none';
|
|
2157
|
+
|
|
2158
|
+
if (filter === 'all' || filter === 'risk') {
|
|
2159
|
+
renderConsolidatedView(projects);
|
|
2160
|
+
renderProjectCards(projects, filter === 'risk');
|
|
2161
|
+
renderStreamBreakdown(breakdown, filter === 'risk');
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
if (filter === 'alerts' || filter === 'risk') {
|
|
2165
|
+
renderAlerts(alerts, filter === 'risk');
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
setPill('ok', 'dashboard atualizado');
|
|
2169
|
+
} catch (e) {
|
|
2170
|
+
setPill('err', 'falha ao carregar dashboard');
|
|
2171
|
+
console.error('refreshCompanionDash error:', e);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function renderConsolidatedView(projects) {
|
|
2176
|
+
const box = $('consolidatedView');
|
|
2177
|
+
if (!box) return;
|
|
2178
|
+
|
|
2179
|
+
const totalCompleted = projects.reduce((sum, p) => sum + p.completedTasks, 0);
|
|
2180
|
+
const totalPending = projects.reduce((sum, p) => sum + p.pendingTasks, 0);
|
|
2181
|
+
const totalCritical = projects.reduce((sum, p) => sum + (p.blockersBySeverity?.CRITICAL || 0), 0);
|
|
2182
|
+
const totalPendingTasks = projects.reduce((sum, p) => sum + p.pendingTasks, 0);
|
|
2183
|
+
const overallCompletion = projects.reduce((sum, p) => sum + p.completionRate, 0) / Math.max(projects.length, 1);
|
|
2184
|
+
|
|
2185
|
+
const kpis = [
|
|
2186
|
+
{ icon: '✅', label: 'Concluídas', value: totalCompleted, unit: 'semana' },
|
|
2187
|
+
{ icon: '⏳', label: 'Pendentes', value: totalPending, unit: 'lembretes' },
|
|
2188
|
+
{ icon: '🚧', label: 'CRITICAL', value: totalCritical, unit: 'bloqueios' },
|
|
2189
|
+
{ icon: '📋', label: 'Taxa Conclusão', value: Math.round(overallCompletion), unit: '%' }
|
|
2190
|
+
];
|
|
2191
|
+
|
|
2192
|
+
box.innerHTML = kpis.map(kpi => `
|
|
2193
|
+
<div class="kpiCard">
|
|
2194
|
+
<div style="font-size: 24px; margin-bottom: 4px;">${kpi.icon}</div>
|
|
2195
|
+
<div style="font-size: 28px; font-weight: 700; color: var(--accent);">${kpi.value}</div>
|
|
2196
|
+
<div style="font-size: 11px; color: var(--textMuted); margin-top: 4px;">${kpi.label}</div>
|
|
2197
|
+
<div style="font-size: 10px; color: var(--textMuted);">${kpi.unit}</div>
|
|
2198
|
+
</div>
|
|
2199
|
+
`).join('');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function renderProjectCards(projects, onlyRisk = false) {
|
|
2203
|
+
const box = $('projectCardsGrid');
|
|
2204
|
+
if (!box) return;
|
|
2205
|
+
|
|
2206
|
+
let filtered = projects;
|
|
2207
|
+
if (onlyRisk) {
|
|
2208
|
+
filtered = projects.filter(p => p.status === 'AT_RISK' || p.status === 'IDLE');
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if (filtered.length === 0) {
|
|
2212
|
+
box.innerHTML = '<div class="help">Nenhum projeto para exibir.</div>';
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
box.innerHTML = filtered.map(p => {
|
|
2217
|
+
const statusColor = p.status === 'IDLE' ? '#666' : p.status === 'AT_RISK' ? '#ff9900' : '#4ade80';
|
|
2218
|
+
const statusText = p.status === 'IDLE' ? 'Inativo' : p.status === 'AT_RISK' ? 'Risco' : 'OK';
|
|
2219
|
+
return `
|
|
2220
|
+
<div class="projectCard" style="border-left-color: ${statusColor}; cursor: pointer;" onclick="window.location.href='/dashboard?project=${encodeURIComponent(p.slug)}'">
|
|
2221
|
+
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
|
2222
|
+
<div style="font-weight: 700; font-size: 13px;">${escapeHtml(p.name)}</div>
|
|
2223
|
+
<span class="pill" style="font-size: 9px; padding: 2px 6px;">${statusText}</span>
|
|
2224
|
+
</div>
|
|
2225
|
+
<div style="display: flex; gap: 12px; margin-bottom: 8px; font-size: 11px;">
|
|
2226
|
+
<div>✓ ${p.completedTasks}/${p.totalTasks}</div>
|
|
2227
|
+
<div>🚧 ${p.openBlockers}</div>
|
|
2228
|
+
</div>
|
|
2229
|
+
<div style="font-size: 10px; color: var(--textMuted);">Atualizado: ${p.lastUpdateAgo}</div>
|
|
2230
|
+
</div>
|
|
2231
|
+
`;
|
|
2232
|
+
}).join('');
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function renderStreamBreakdown(breakdown, onlyRisk = false) {
|
|
2236
|
+
const box = $('streamBreakdown');
|
|
2237
|
+
if (!box) return;
|
|
2238
|
+
|
|
2239
|
+
if (breakdown.length === 0) {
|
|
2240
|
+
box.innerHTML = '<div class="help">Nenhum stream para exibir.</div>';
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
let html = '';
|
|
2245
|
+
for (const proj of breakdown) {
|
|
2246
|
+
let streams = proj.streams;
|
|
2247
|
+
if (onlyRisk) {
|
|
2248
|
+
streams = streams.filter(s => s.blockersCount > 0);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
if (streams.length === 0) continue;
|
|
2252
|
+
|
|
2253
|
+
html += `<div style="margin-bottom: 12px;">
|
|
2254
|
+
<div style="font-weight: 700; font-size: 12px; margin-bottom: 4px; color: var(--accent);">${escapeHtml(proj.projectName)}</div>`;
|
|
2255
|
+
|
|
2256
|
+
for (const s of streams) {
|
|
2257
|
+
const hasBlockers = s.blockersCount > 0;
|
|
2258
|
+
html += `
|
|
2259
|
+
<div class="streamItem" style="background: ${hasBlockers ? 'rgba(239,68,68,0.05)' : 'transparent'};">
|
|
2260
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
2261
|
+
<div style="font-size: 11px;">${escapeHtml(s.streamName)}</div>
|
|
2262
|
+
<div style="font-size: 10px; color: var(--textMuted);">✓ ${s.completedTasks}/${s.totalTasks}</div>
|
|
2263
|
+
</div>
|
|
2264
|
+
${hasBlockers ? `<div style="font-size: 10px; margin-top: 4px; color: #ef4444;">🚧 ${s.blockersCount} bloqueio(s)</div>` : ''}
|
|
2265
|
+
</div>
|
|
2266
|
+
`;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
html += '</div>';
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
box.innerHTML = html || '<div class="help">Nenhum stream para exibir.</div>';
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function renderAlerts(alerts, onlyHighSeverity = false) {
|
|
2276
|
+
const box = $('alertsView');
|
|
2277
|
+
if (!box) return;
|
|
2278
|
+
|
|
2279
|
+
let filtered = alerts;
|
|
2280
|
+
if (onlyHighSeverity) {
|
|
2281
|
+
filtered = alerts.filter(a => a.severity === 'CRITICAL' || a.severity === 'HIGH');
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
if (filtered.length === 0) {
|
|
2285
|
+
box.innerHTML = '<div class="help">Nenhum alerta para exibir.</div>';
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
box.innerHTML = filtered.slice(0, 20).map(a => {
|
|
2290
|
+
const severityColor = a.severity === 'CRITICAL' ? '#ef4444' : a.severity === 'HIGH' ? '#ff9900' : '#facc15';
|
|
2291
|
+
return `
|
|
2292
|
+
<div class="alertItem" style="border-left-color: ${severityColor};">
|
|
2293
|
+
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px;">
|
|
2294
|
+
<span style="font-weight: 700; color: ${severityColor};">${a.severity}</span>
|
|
2295
|
+
<span style="color: var(--textMuted);">${a.type}</span>
|
|
2296
|
+
</div>
|
|
2297
|
+
<div style="font-size: 11px; margin-top: 4px;">${escapeHtml(a.message)}</div>
|
|
2298
|
+
</div>
|
|
2299
|
+
`;
|
|
2300
|
+
}).join('');
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
window.filterCompanionView = function(filter) {
|
|
2304
|
+
document.querySelectorAll('.companionTabs .tab').forEach(tab => {
|
|
2305
|
+
tab.classList.remove('active');
|
|
2306
|
+
});
|
|
2307
|
+
document.querySelector(`[data-filter="${filter}"]`).classList.add('active');
|
|
2308
|
+
refreshCompanionDash();
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2133
2311
|
async function doHealth() {
|
|
2134
2312
|
try {
|
|
2135
2313
|
saveLocal();
|
|
@@ -2637,13 +2815,7 @@
|
|
|
2637
2815
|
}
|
|
2638
2816
|
|
|
2639
2817
|
if (isCompanionPage) {
|
|
2640
|
-
await
|
|
2641
|
-
await refreshQualityScore();
|
|
2642
|
-
await refreshExecutiveSummary();
|
|
2643
|
-
await refreshAnomalies();
|
|
2644
|
-
await refreshRiskRadar();
|
|
2645
|
-
await refreshIncidents();
|
|
2646
|
-
await refreshHeatmap();
|
|
2818
|
+
await refreshCompanionDash();
|
|
2647
2819
|
return;
|
|
2648
2820
|
}
|
|
2649
2821
|
|
|
@@ -2723,6 +2895,7 @@
|
|
|
2723
2895
|
window.refreshExecutiveSummary = refreshExecutiveSummary;
|
|
2724
2896
|
window.refreshAnomalies = refreshAnomalies;
|
|
2725
2897
|
window.refreshRiskRadar = refreshRiskRadar;
|
|
2898
|
+
window.refreshCompanionDash = refreshCompanionDash;
|
|
2726
2899
|
window.copyOut = copyOut;
|
|
2727
2900
|
window.copyPath = copyPath;
|
|
2728
2901
|
window.openSelected = openSelected;
|
package/cli/web.js
CHANGED
|
@@ -1797,10 +1797,10 @@ 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">Acompanhamento de projetos e frentes em tempo real.</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 Dashboard</button>
|
|
1804
1804
|
</div>
|
|
1805
1805
|
</section>
|
|
1806
1806
|
|
|
@@ -1808,86 +1808,46 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1808
1808
|
📊 Ver / Gerar Relatórios →
|
|
1809
1809
|
</button>
|
|
1810
1810
|
|
|
1811
|
-
<!--
|
|
1812
|
-
<div
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
<
|
|
1816
|
-
|
|
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>
|
|
1811
|
+
<!-- PROJECTS OVERVIEW -->
|
|
1812
|
+
<div class="companionTabs">
|
|
1813
|
+
<button class="tab active" data-filter="all" onclick="window.filterCompanionView('all')">○ Todos</button>
|
|
1814
|
+
<button class="tab" data-filter="alerts" onclick="window.filterCompanionView('alerts')">● Alertas</button>
|
|
1815
|
+
<button class="tab" data-filter="risk" onclick="window.filterCompanionView('risk')">⚠ Risco Alto</button>
|
|
1816
|
+
</div>
|
|
1845
1817
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
</section>
|
|
1818
|
+
<!-- CONSOLIDATED VIEW -->
|
|
1819
|
+
<div id="consolidatedViewBox" style="display: none;">
|
|
1820
|
+
<section class="panel">
|
|
1821
|
+
<div class="panelHead"><b>Visão Consolidada</b></div>
|
|
1822
|
+
<div class="panelBody">
|
|
1823
|
+
<div class="consolidatedView" id="consolidatedView" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;"></div>
|
|
1824
|
+
</div>
|
|
1825
|
+
</section>
|
|
1826
|
+
</div>
|
|
1856
1827
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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>
|
|
1828
|
+
<!-- PROJECT CARDS GRID -->
|
|
1829
|
+
<div id="projectCardsBox" style="display: none;">
|
|
1830
|
+
<div class="projectCardsGrid" id="projectCardsGrid"></div>
|
|
1831
|
+
</div>
|
|
1867
1832
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
<div id="anomaliesBox"></div>
|
|
1875
|
-
</div>
|
|
1876
|
-
</section>
|
|
1833
|
+
<!-- STREAM BREAKDOWN -->
|
|
1834
|
+
<div id="streamBreakdownBox" style="display: none;">
|
|
1835
|
+
<section class="panel">
|
|
1836
|
+
<div class="panelHead"><b>Detalhamento por Stream</b></div>
|
|
1837
|
+
<div class="panelBody">
|
|
1838
|
+
<div id="streamBreakdown"></div>
|
|
1877
1839
|
</div>
|
|
1878
|
-
</
|
|
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>
|
|
1888
|
-
|
|
1889
|
-
</div>
|
|
1840
|
+
</section>
|
|
1841
|
+
</div>
|
|
1890
1842
|
|
|
1843
|
+
<!-- ALERTS VIEW -->
|
|
1844
|
+
<div id="alertsViewBox" style="display: none;">
|
|
1845
|
+
<section class="panel">
|
|
1846
|
+
<div class="panelHead"><b>Alertas Prioritários</b></div>
|
|
1847
|
+
<div class="panelBody">
|
|
1848
|
+
<div id="alertsView"></div>
|
|
1849
|
+
</div>
|
|
1850
|
+
</section>
|
|
1891
1851
|
</div>
|
|
1892
1852
|
</div>
|
|
1893
1853
|
</main>
|
|
@@ -2850,6 +2810,310 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2850
2810
|
return safeJson(res, 200, { ok: true, items });
|
|
2851
2811
|
}
|
|
2852
2812
|
|
|
2813
|
+
// /api/companion/projects-summary: Consolidate project KPIs
|
|
2814
|
+
if (req.url === '/api/companion/projects-summary') {
|
|
2815
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
2816
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
const now = Date.now();
|
|
2820
|
+
const projectMap = {}; // projectSlug -> { name, totalTasks, completedTasks, pendingTasks, blockers, streams, lastUpdateAgo, status }
|
|
2821
|
+
|
|
2822
|
+
// 1. Load tasks
|
|
2823
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2824
|
+
if (exists(taskFile)) {
|
|
2825
|
+
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2826
|
+
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2827
|
+
|
|
2828
|
+
for (const t of tasks) {
|
|
2829
|
+
const slug = String(t.projectSlug || '').trim();
|
|
2830
|
+
if (!slug) continue;
|
|
2831
|
+
|
|
2832
|
+
if (!projectMap[slug]) {
|
|
2833
|
+
projectMap[slug] = {
|
|
2834
|
+
slug, name: slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
2835
|
+
openBlockers: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
2836
|
+
streams: new Set(), lastUpdateMs: now, status: 'ON_TRACK'
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
projectMap[slug].totalTasks++;
|
|
2841
|
+
if (t.status === 'COMPLETED') {
|
|
2842
|
+
projectMap[slug].completedTasks++;
|
|
2843
|
+
} else {
|
|
2844
|
+
projectMap[slug].pendingTasks++;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
const streamSlug = String(t.streamSlug || '').trim();
|
|
2848
|
+
if (streamSlug) {
|
|
2849
|
+
projectMap[slug].streams.add(streamSlug);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
if (t.createdAt) {
|
|
2853
|
+
const taskTime = Date.parse(t.createdAt);
|
|
2854
|
+
if (taskTime > projectMap[slug].lastUpdateMs) {
|
|
2855
|
+
projectMap[slug].lastUpdateMs = taskTime;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// 2. Load blockers
|
|
2862
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2863
|
+
if (exists(blockerFile)) {
|
|
2864
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
2865
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
2866
|
+
|
|
2867
|
+
for (const b of blockers) {
|
|
2868
|
+
const slug = String(b.projectSlug || '').trim();
|
|
2869
|
+
if (!slug) continue;
|
|
2870
|
+
|
|
2871
|
+
if (!projectMap[slug]) {
|
|
2872
|
+
projectMap[slug] = {
|
|
2873
|
+
slug, name: slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
2874
|
+
openBlockers: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
2875
|
+
streams: new Set(), lastUpdateMs: now, status: 'ON_TRACK'
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
2880
|
+
projectMap[slug].openBlockers++;
|
|
2881
|
+
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
2882
|
+
if (projectMap[slug].blockersBySeverity[sev] !== undefined) {
|
|
2883
|
+
projectMap[slug].blockersBySeverity[sev]++;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
if (b.createdAt) {
|
|
2888
|
+
const blockerTime = Date.parse(b.createdAt);
|
|
2889
|
+
if (blockerTime > projectMap[slug].lastUpdateMs) {
|
|
2890
|
+
projectMap[slug].lastUpdateMs = blockerTime;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// Helper: format time ago
|
|
2897
|
+
const formatAgo = (ms) => {
|
|
2898
|
+
const age = now - ms;
|
|
2899
|
+
const secs = Math.floor(age / 1000);
|
|
2900
|
+
if (secs < 60) return 'just now';
|
|
2901
|
+
const mins = Math.floor(secs / 60);
|
|
2902
|
+
if (mins < 60) return mins + 'm ago';
|
|
2903
|
+
const hours = Math.floor(mins / 60);
|
|
2904
|
+
if (hours < 24) return hours + 'h ago';
|
|
2905
|
+
const days = Math.floor(hours / 24);
|
|
2906
|
+
return days + 'd ago';
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// Calculate status and completion rate for each project
|
|
2910
|
+
const projects = [];
|
|
2911
|
+
for (const [slug, proj] of Object.entries(projectMap)) {
|
|
2912
|
+
const completionRate = proj.totalTasks > 0 ? Math.round((proj.completedTasks / proj.totalTasks) * 100) : 0;
|
|
2913
|
+
let status = 'ON_TRACK';
|
|
2914
|
+
|
|
2915
|
+
if (proj.blockersBySeverity.CRITICAL > 0) status = 'AT_RISK';
|
|
2916
|
+
if (proj.blockersBySeverity.HIGH >= 3) status = 'AT_RISK';
|
|
2917
|
+
if (proj.pendingTasks > 15) status = 'AT_RISK';
|
|
2918
|
+
|
|
2919
|
+
const ageDays = Math.floor((now - proj.lastUpdateMs) / (24 * 60 * 60 * 1000));
|
|
2920
|
+
if (ageDays > 14) status = 'IDLE';
|
|
2921
|
+
|
|
2922
|
+
projects.push({
|
|
2923
|
+
slug: proj.slug,
|
|
2924
|
+
name: proj.name,
|
|
2925
|
+
totalTasks: proj.totalTasks,
|
|
2926
|
+
completedTasks: proj.completedTasks,
|
|
2927
|
+
pendingTasks: proj.pendingTasks,
|
|
2928
|
+
completionRate,
|
|
2929
|
+
openBlockers: proj.openBlockers,
|
|
2930
|
+
blockersBySeverity: proj.blockersBySeverity,
|
|
2931
|
+
streams: Array.from(proj.streams),
|
|
2932
|
+
lastUpdateAgo: formatAgo(proj.lastUpdateMs),
|
|
2933
|
+
status
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
return safeJson(res, 200, { ok: true, projects });
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// /api/companion/streams-breakdown: Task/blocker breakdown by stream within each project
|
|
2941
|
+
if (req.url === '/api/companion/streams-breakdown') {
|
|
2942
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
2943
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
const breakdownMap = {}; // projectSlug -> { projectName, streams: [...] }
|
|
2947
|
+
|
|
2948
|
+
// 1. Load tasks grouped by project & stream
|
|
2949
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2950
|
+
if (exists(taskFile)) {
|
|
2951
|
+
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2952
|
+
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2953
|
+
|
|
2954
|
+
for (const t of tasks) {
|
|
2955
|
+
const pSlug = String(t.projectSlug || '').trim();
|
|
2956
|
+
const sSlug = String(t.streamSlug || 'default').trim();
|
|
2957
|
+
if (!pSlug) continue;
|
|
2958
|
+
|
|
2959
|
+
if (!breakdownMap[pSlug]) {
|
|
2960
|
+
breakdownMap[pSlug] = { projectName: pSlug, streams: {} };
|
|
2961
|
+
}
|
|
2962
|
+
if (!breakdownMap[pSlug].streams[sSlug]) {
|
|
2963
|
+
breakdownMap[pSlug].streams[sSlug] = {
|
|
2964
|
+
streamName: sSlug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
2965
|
+
blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
breakdownMap[pSlug].streams[sSlug].totalTasks++;
|
|
2970
|
+
if (t.status === 'COMPLETED') {
|
|
2971
|
+
breakdownMap[pSlug].streams[sSlug].completedTasks++;
|
|
2972
|
+
} else {
|
|
2973
|
+
breakdownMap[pSlug].streams[sSlug].pendingTasks++;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// 2. Load blockers grouped by project & stream
|
|
2979
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2980
|
+
if (exists(blockerFile)) {
|
|
2981
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
2982
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
2983
|
+
|
|
2984
|
+
for (const b of blockers) {
|
|
2985
|
+
const pSlug = String(b.projectSlug || '').trim();
|
|
2986
|
+
const sSlug = String(b.streamSlug || 'default').trim();
|
|
2987
|
+
if (!pSlug) continue;
|
|
2988
|
+
|
|
2989
|
+
if (!breakdownMap[pSlug]) {
|
|
2990
|
+
breakdownMap[pSlug] = { projectName: pSlug, streams: {} };
|
|
2991
|
+
}
|
|
2992
|
+
if (!breakdownMap[pSlug].streams[sSlug]) {
|
|
2993
|
+
breakdownMap[pSlug].streams[sSlug] = {
|
|
2994
|
+
streamName: sSlug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
2995
|
+
blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
3000
|
+
breakdownMap[pSlug].streams[sSlug].blockersCount++;
|
|
3001
|
+
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
3002
|
+
if (breakdownMap[pSlug].streams[sSlug].blockersBySeverity[sev] !== undefined) {
|
|
3003
|
+
breakdownMap[pSlug].streams[sSlug].blockersBySeverity[sev]++;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// Convert to array format
|
|
3010
|
+
const breakdown = [];
|
|
3011
|
+
for (const [pSlug, pData] of Object.entries(breakdownMap)) {
|
|
3012
|
+
const streams = [];
|
|
3013
|
+
for (const [sSlug, sData] of Object.entries(pData.streams)) {
|
|
3014
|
+
streams.push({
|
|
3015
|
+
streamName: sData.streamName,
|
|
3016
|
+
totalTasks: sData.totalTasks,
|
|
3017
|
+
completedTasks: sData.completedTasks,
|
|
3018
|
+
pendingTasks: sData.pendingTasks,
|
|
3019
|
+
blockersCount: sData.blockersCount,
|
|
3020
|
+
blockersBySeverity: sData.blockersBySeverity
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
breakdown.push({
|
|
3024
|
+
projectSlug: pSlug,
|
|
3025
|
+
projectName: pData.projectName,
|
|
3026
|
+
streams: streams.sort((a, b) => b.blockersCount - a.blockersCount)
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
return safeJson(res, 200, { ok: true, breakdown });
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// /api/companion/alerts: Prioritized alerts by severity
|
|
3034
|
+
if (req.url === '/api/companion/alerts') {
|
|
3035
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3036
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
const now = Date.now();
|
|
3040
|
+
const alerts = [];
|
|
3041
|
+
|
|
3042
|
+
// 1. Check for old blockers
|
|
3043
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
3044
|
+
if (exists(blockerFile)) {
|
|
3045
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
3046
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
3047
|
+
|
|
3048
|
+
for (const b of blockers) {
|
|
3049
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
3050
|
+
const createdTime = b.createdAt ? Date.parse(b.createdAt) : now;
|
|
3051
|
+
const ageDays = Math.floor((now - createdTime) / (24 * 60 * 60 * 1000));
|
|
3052
|
+
|
|
3053
|
+
let severity = 'MEDIUM';
|
|
3054
|
+
if (String(b.severity || '').toUpperCase() === 'CRITICAL') severity = 'CRITICAL';
|
|
3055
|
+
else if (ageDays > 7) severity = 'HIGH';
|
|
3056
|
+
|
|
3057
|
+
alerts.push({
|
|
3058
|
+
severity,
|
|
3059
|
+
type: 'old_blocker',
|
|
3060
|
+
projectSlug: String(b.projectSlug || '').trim(),
|
|
3061
|
+
streamName: String(b.streamSlug || '').trim(),
|
|
3062
|
+
message: `Bloqueio: ${b.title} (${ageDays} dias)`,
|
|
3063
|
+
age: ageDays,
|
|
3064
|
+
createdAt: b.createdAt
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// 2. Check for stale projects
|
|
3071
|
+
const base = path.join(workspaceDir, 'data', 'Clients');
|
|
3072
|
+
if (exists(base)) {
|
|
3073
|
+
const stack = [base];
|
|
3074
|
+
while (stack.length) {
|
|
3075
|
+
const dirp = stack.pop();
|
|
3076
|
+
const entries = fs.readdirSync(dirp, { withFileTypes: true });
|
|
3077
|
+
for (const ent of entries) {
|
|
3078
|
+
const full = path.join(dirp, ent.name);
|
|
3079
|
+
if (ent.isDirectory()) stack.push(full);
|
|
3080
|
+
else if (ent.isFile() && ent.name === 'status.json') {
|
|
3081
|
+
const doc = readJsonOrNull(full) || {};
|
|
3082
|
+
const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
|
|
3083
|
+
if (doc.active !== false) {
|
|
3084
|
+
const lastUpdated = doc.lastUpdated ? Date.parse(doc.lastUpdated) : null;
|
|
3085
|
+
if (lastUpdated) {
|
|
3086
|
+
const ageDays = Math.floor((now - lastUpdated) / (24 * 60 * 60 * 1000));
|
|
3087
|
+
if (ageDays > 14) {
|
|
3088
|
+
alerts.push({
|
|
3089
|
+
severity: ageDays > 30 ? 'CRITICAL' : 'HIGH',
|
|
3090
|
+
type: 'stale_project',
|
|
3091
|
+
projectSlug: slug,
|
|
3092
|
+
streamName: '',
|
|
3093
|
+
message: `Projeto inativo por ${ageDays} dias`,
|
|
3094
|
+
age: ageDays,
|
|
3095
|
+
createdAt: doc.lastUpdated
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// Sort by severity (CRITICAL > HIGH > MEDIUM) then by age
|
|
3106
|
+
const severityOrder = { CRITICAL: 3, HIGH: 2, MEDIUM: 1 };
|
|
3107
|
+
alerts.sort((a, b) => {
|
|
3108
|
+
const sA = severityOrder[a.severity] || 0;
|
|
3109
|
+
const sB = severityOrder[b.severity] || 0;
|
|
3110
|
+
if (sA !== sB) return sB - sA;
|
|
3111
|
+
return (b.age || 0) - (a.age || 0);
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
return safeJson(res, 200, { ok: true, alerts });
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2853
3117
|
if (req.url === '/api/incidents/resolve') {
|
|
2854
3118
|
const title = payload.title;
|
|
2855
3119
|
const index = Number.isInteger(payload.index) ? payload.index : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cccarv82/freya",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "Personal AI Assistant with local-first persistence",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"health": "node scripts/validate-data.js && node scripts/validate-structure.js",
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
"pdf-lib": "^1.17.1",
|
|
36
36
|
"sql.js": "^1.12.0"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|