@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 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
- const oli = !li ? line.match(/^[ \t]*\d+\.[ \t]+(.*)$/) : null;
143
- if (li || oli) {
142
+ if (li) {
144
143
  if (!inList) { html += '<ul class="md-ul">'; inList = true; }
145
- const content = inlineFormat((li || oli)[1]);
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('- \u{1F4DD} **Registrar no log:** ' + t + (m[1].length > 140 ? '...' : ''));
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('- \u2705 **Criar tarefa:** ' + desc + pri);
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('- \u{1F6A7} **Registrar blocker:** ' + title + sev);
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
- lines.push('- \u{1F4CA} **Sugerir relatorio:** ' + m[1]);
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 '- ' + icon + ' **Registrar no log:** ' + t + (String(a.text || '').length > 140 ? '...' : '');
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 '- ' + icon + ' **Criar tarefa:** ' + desc + pri + cat;
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 '- ' + icon + ' **Registrar blocker:** ' + title + sev;
261
+ return num + '. ' + icon + ' **Registrar blocker:** ' + title + sev;
257
262
  }
258
263
  if (type === 'suggestreport') {
259
- return '- ' + icon + ' **Sugerir relatorio:** ' + String(a.name || a.reportType || '');
264
+ return num + '. ' + icon + ' **Sugerir relatorio:** ' + String(a.name || a.reportType || '');
260
265
  }
261
266
  if (type === 'oraclequery') {
262
- return '- ' + icon + ' **Consultar oracle:** ' + String(a.query || '').slice(0, 120);
267
+ return num + '. ' + icon + ' **Consultar oracle:** ' + String(a.query || '').slice(0, 120);
263
268
  }
264
- return '- \u2022 **' + String(a.type || 'acao') + '**';
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 refreshHealthChecklist();
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">Painel rápido para gerar relatórios e checar pendências.</div>
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="refreshHealthChecklist()">Atualizar Painel</button>
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
- <!-- BENTO GRID LAYOUT -->
1812
- <div style="display: grid; grid-template-columns: 1fr 1.5fr; gap: 24px; align-items: start;">
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>
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
- <!-- 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>
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
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
1858
- <section class="panel">
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>
1828
+ <!-- PROJECT CARDS GRID -->
1829
+ <div id="projectCardsBox" style="display: none;">
1830
+ <div class="projectCardsGrid" id="projectCardsGrid"></div>
1831
+ </div>
1867
1832
 
1868
- <section class="panel">
1869
- <div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
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>
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
- </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>
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.13.4",
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
+ }