@cccarv82/freya 2.14.0 → 2.15.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.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');
@@ -336,6 +341,11 @@
336
341
  persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
337
342
  }
338
343
 
344
+ // show the thread now that it has content
345
+ thread.style.display = 'flex';
346
+ thread.style.padding = '12px';
347
+ thread.style.borderTop = '1px solid var(--border)';
348
+
339
349
  // keep newest in view
340
350
  try {
341
351
  thread.scrollTop = thread.scrollHeight;
@@ -472,8 +482,8 @@
472
482
  const thread = $('chatThread');
473
483
  if (!thread) return;
474
484
  const hasContent = thread.children.length > 0;
485
+ thread.style.display = hasContent ? 'flex' : 'none';
475
486
  thread.style.padding = hasContent ? '12px' : '0';
476
- thread.style.maxHeight = hasContent ? '280px' : '0';
477
487
  thread.style.borderTop = hasContent ? '1px solid var(--border)' : 'none';
478
488
  }
479
489
 
@@ -1426,6 +1436,7 @@
1426
1436
  const health = $('railCompanion');
1427
1437
  const graph = $('railGraph');
1428
1438
  const docs = $('railDocs');
1439
+ const kanban = $('railKanban');
1429
1440
 
1430
1441
  const curPage = (document.body && document.body.dataset) ? document.body.dataset.page : null;
1431
1442
  const isDashboard = !curPage || curPage === 'dashboard';
@@ -1455,6 +1466,11 @@
1455
1466
  if (curPage !== 'companion') window.location.href = '/companion';
1456
1467
  };
1457
1468
  }
1469
+ if (kanban) {
1470
+ kanban.onclick = () => {
1471
+ if (curPage !== 'kanban') window.location.href = '/kanban';
1472
+ };
1473
+ }
1458
1474
  if (tl) {
1459
1475
  tl.onclick = () => {
1460
1476
  if (curPage !== 'timeline') window.location.href = '/timeline';
@@ -2130,172 +2146,179 @@
2130
2146
  }
2131
2147
  }
2132
2148
 
2149
+ // New Companion Dashboard functions
2133
2150
  async function refreshCompanionDash() {
2151
+ const filter = document.querySelector('.companionTabs .tab.active')?.dataset?.filter || 'all';
2134
2152
  try {
2135
- setPill('run', 'carregando dashboard...');
2136
- const [pRes, sRes, aRes] = await Promise.all([
2153
+ const [prj, brk, alts] = await Promise.all([
2137
2154
  api('/api/companion/projects-summary', { dir: dirOrDefault() }),
2138
2155
  api('/api/companion/streams-breakdown', { dir: dirOrDefault() }),
2139
2156
  api('/api/companion/alerts', { dir: dirOrDefault() })
2140
2157
  ]);
2141
- if (!pRes || !pRes.ok || !sRes || !sRes.ok || !aRes || !aRes.ok) {
2142
- setPill('err', 'Falha ao carregar dashboard');
2143
- return;
2158
+
2159
+ const projects = (prj && prj.projects) || [];
2160
+ const breakdown = (brk && brk.breakdown) || [];
2161
+ const alerts = (alts && alts.alerts) || [];
2162
+
2163
+ // Show/hide sections based on filter
2164
+ $('consolidatedViewBox').style.display = filter === 'all' ? 'block' : 'none';
2165
+ $('projectCardsBox').style.display = filter === 'all' ? 'block' : 'none';
2166
+ $('streamBreakdownBox').style.display = filter === 'all' ? 'block' : 'none';
2167
+ $('alertsViewBox').style.display = filter === 'alerts' || filter === 'risk' ? 'block' : 'none';
2168
+
2169
+ if (filter === 'all' || filter === 'risk') {
2170
+ renderConsolidatedView(projects);
2171
+ renderProjectCards(projects, filter === 'risk');
2172
+ renderStreamBreakdown(breakdown, filter === 'risk');
2144
2173
  }
2145
- renderConsolidatedView(pRes.projects || []);
2146
- renderProjectCards(pRes.projects || []);
2147
- renderStreamBreakdown(sRes.items || []);
2148
- renderAlerts(aRes.alerts || []);
2149
- setPill('ok', 'dashboard pronto');
2174
+
2175
+ if (filter === 'alerts' || filter === 'risk') {
2176
+ renderAlerts(alerts, filter === 'risk');
2177
+ }
2178
+
2179
+ setPill('ok', 'dashboard atualizado');
2150
2180
  } catch (e) {
2151
- setPill('err', 'Erro ao carregar dashboard');
2181
+ setPill('err', 'falha ao carregar dashboard');
2152
2182
  console.error('refreshCompanionDash error:', e);
2153
2183
  }
2154
2184
  }
2155
2185
 
2156
2186
  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>
2187
+ const box = $('consolidatedView');
2188
+ if (!box) return;
2189
+
2190
+ const totalCompleted = projects.reduce((sum, p) => sum + p.completedTasks, 0);
2191
+ const totalPending = projects.reduce((sum, p) => sum + p.pendingTasks, 0);
2192
+ const totalCritical = projects.reduce((sum, p) => sum + (p.blockersBySeverity?.CRITICAL || 0), 0);
2193
+ const totalPendingTasks = projects.reduce((sum, p) => sum + p.pendingTasks, 0);
2194
+ const overallCompletion = projects.reduce((sum, p) => sum + p.completionRate, 0) / Math.max(projects.length, 1);
2195
+
2196
+ const kpis = [
2197
+ { icon: '✅', label: 'Concluídas', value: totalCompleted, unit: 'semana' },
2198
+ { icon: '⏳', label: 'Pendentes', value: totalPending, unit: 'lembretes' },
2199
+ { icon: '🚧', label: 'CRITICAL', value: totalCritical, unit: 'bloqueios' },
2200
+ { icon: '📋', label: 'Taxa Conclusão', value: Math.round(overallCompletion), unit: '%' }
2201
+ ];
2202
+
2203
+ box.innerHTML = kpis.map(kpi => `
2180
2204
  <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>
2205
+ <div style="font-size: 24px; margin-bottom: 4px;">${kpi.icon}</div>
2206
+ <div style="font-size: 28px; font-weight: 700; color: var(--accent);">${kpi.value}</div>
2207
+ <div style="font-size: 11px; color: var(--textMuted); margin-top: 4px;">${kpi.label}</div>
2208
+ <div style="font-size: 10px; color: var(--textMuted);">${kpi.unit}</div>
2183
2209
  </div>
2184
- `;
2210
+ `).join('');
2185
2211
  }
2186
2212
 
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';
2213
+ function renderProjectCards(projects, onlyRisk = false) {
2214
+ const box = $('projectCardsGrid');
2215
+ if (!box) return;
2216
+
2217
+ let filtered = projects;
2218
+ if (onlyRisk) {
2219
+ filtered = projects.filter(p => p.status === 'AT_RISK' || p.status === 'IDLE');
2220
+ }
2221
+
2222
+ if (filtered.length === 0) {
2223
+ box.innerHTML = '<div class="help">Nenhum projeto para exibir.</div>';
2224
+ return;
2225
+ }
2226
+
2227
+ box.innerHTML = filtered.map(p => {
2228
+ const statusColor = p.status === 'IDLE' ? '#666' : p.status === 'AT_RISK' ? '#ff9900' : '#4ade80';
2229
+ const statusText = p.status === 'IDLE' ? 'Inativo' : p.status === 'AT_RISK' ? 'Risco' : 'OK';
2194
2230
  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>
2231
+ <div class="projectCard" style="border-left-color: ${statusColor}; cursor: pointer;" onclick="window.location.href='/dashboard?project=${encodeURIComponent(p.slug)}'">
2232
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
2233
+ <div style="font-weight: 700; font-size: 13px;">${escapeHtml(p.name)}</div>
2234
+ <span class="pill" style="font-size: 9px; padding: 2px 6px;">${statusText}</span>
2212
2235
  </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>
2236
+ <div style="display: flex; gap: 12px; margin-bottom: 8px; font-size: 11px;">
2237
+ <div>✓ ${p.completedTasks}/${p.totalTasks}</div>
2238
+ <div>🚧 ${p.openBlockers}</div>
2216
2239
  </div>
2240
+ <div style="font-size: 10px; color: var(--textMuted);">Atualizado: ${p.lastUpdateAgo}</div>
2217
2241
  </div>
2218
2242
  `;
2219
2243
  }).join('');
2220
- grid.innerHTML = cards || '<div class="help">Nenhum projeto cadastrado.</div>';
2221
2244
  }
2222
2245
 
2223
- function renderStreamBreakdown(data) {
2224
- const el = $('streamBreakdown');
2225
- if (!el) return;
2246
+ function renderStreamBreakdown(breakdown, onlyRisk = false) {
2247
+ const box = $('streamBreakdown');
2248
+ if (!box) return;
2249
+
2250
+ if (breakdown.length === 0) {
2251
+ box.innerHTML = '<div class="help">Nenhum stream para exibir.</div>';
2252
+ return;
2253
+ }
2254
+
2226
2255
  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>
2256
+ for (const proj of breakdown) {
2257
+ let streams = proj.streams;
2258
+ if (onlyRisk) {
2259
+ streams = streams.filter(s => s.blockersCount > 0);
2260
+ }
2261
+
2262
+ if (streams.length === 0) continue;
2263
+
2264
+ html += `<div style="margin-bottom: 12px;">
2265
+ <div style="font-weight: 700; font-size: 12px; margin-bottom: 4px; color: var(--accent);">${escapeHtml(proj.projectName)}</div>`;
2266
+
2267
+ for (const s of streams) {
2268
+ const hasBlockers = s.blockersCount > 0;
2269
+ html += `
2270
+ <div class="streamItem" style="background: ${hasBlockers ? 'rgba(239,68,68,0.05)' : 'transparent'};">
2271
+ <div style="display: flex; justify-content: space-between; align-items: center;">
2272
+ <div style="font-size: 11px;">${escapeHtml(s.streamName)}</div>
2273
+ <div style="font-size: 10px; color: var(--textMuted);">✓ ${s.completedTasks}/${s.totalTasks}</div>
2238
2274
  </div>
2275
+ ${hasBlockers ? `<div style="font-size: 10px; margin-top: 4px; color: #ef4444;">🚧 ${s.blockersCount} bloqueio(s)</div>` : ''}
2239
2276
  </div>
2240
- </div>`;
2277
+ `;
2241
2278
  }
2242
- html += `</div>`;
2279
+
2280
+ html += '</div>';
2243
2281
  }
2244
- el.innerHTML = html || '<div class="help">Nenhuma frente cadastrada.</div>';
2282
+
2283
+ box.innerHTML = html || '<div class="help">Nenhum stream para exibir.</div>';
2245
2284
  }
2246
2285
 
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';
2286
+ function renderAlerts(alerts, onlyHighSeverity = false) {
2287
+ const box = $('alertsView');
2288
+ if (!box) return;
2289
+
2290
+ let filtered = alerts;
2291
+ if (onlyHighSeverity) {
2292
+ filtered = alerts.filter(a => a.severity === 'CRITICAL' || a.severity === 'HIGH');
2293
+ }
2294
+
2295
+ if (filtered.length === 0) {
2296
+ box.innerHTML = '<div class="help">Nenhum alerta para exibir.</div>';
2254
2297
  return;
2255
2298
  }
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>` : ''}
2299
+
2300
+ box.innerHTML = filtered.slice(0, 20).map(a => {
2301
+ const severityColor = a.severity === 'CRITICAL' ? '#ef4444' : a.severity === 'HIGH' ? '#ff9900' : '#facc15';
2302
+ return `
2303
+ <div class="alertItem" style="border-left-color: ${severityColor};">
2304
+ <div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px;">
2305
+ <span style="font-weight: 700; color: ${severityColor};">${a.severity}</span>
2306
+ <span style="color: var(--textMuted);">${a.type}</span>
2265
2307
  </div>
2308
+ <div style="font-size: 11px; margin-top: 4px;">${escapeHtml(a.message)}</div>
2266
2309
  </div>
2267
- </div>`;
2310
+ `;
2268
2311
  }).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
2312
  }
2298
2313
 
2314
+ window.filterCompanionView = function(filter) {
2315
+ document.querySelectorAll('.companionTabs .tab').forEach(tab => {
2316
+ tab.classList.remove('active');
2317
+ });
2318
+ document.querySelector(`[data-filter="${filter}"]`).classList.add('active');
2319
+ refreshCompanionDash();
2320
+ };
2321
+
2299
2322
  async function doHealth() {
2300
2323
  try {
2301
2324
  saveLocal();
@@ -2757,6 +2780,7 @@
2757
2780
  const isTimelinePage = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
2758
2781
  const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
2759
2782
  const isGraphPage = document.body && document.body.dataset && document.body.dataset.page === 'graph';
2783
+ const isKanbanPage = document.body && document.body.dataset && document.body.dataset.page === 'kanban';
2760
2784
 
2761
2785
  // Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
2762
2786
  (async () => {
@@ -2803,13 +2827,12 @@
2803
2827
  }
2804
2828
 
2805
2829
  if (isCompanionPage) {
2806
- await refreshHealthChecklist();
2807
- await refreshQualityScore();
2808
- await refreshExecutiveSummary();
2809
- await refreshAnomalies();
2810
- await refreshRiskRadar();
2811
- await refreshIncidents();
2812
- await refreshHeatmap();
2830
+ await refreshCompanionDash();
2831
+ return;
2832
+ }
2833
+
2834
+ if (isKanbanPage) {
2835
+ await loadKanban();
2813
2836
  return;
2814
2837
  }
2815
2838
 
@@ -2841,16 +2864,20 @@
2841
2864
  if (typeof saveAndPlan === 'function') saveAndPlan();
2842
2865
  return;
2843
2866
  }
2844
- // Ctrl/Cmd+K: Focus search/input on current page
2867
+ // Ctrl/Cmd+K: Open quick-add modal
2845
2868
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
2846
2869
  e.preventDefault();
2847
- const target = $('inboxText') || $('reportsFilter') || $('projectsFilter') || $('timelineFilter');
2848
- if (target) target.focus();
2870
+ openQuickAdd();
2849
2871
  return;
2850
2872
  }
2851
- // Escape: Blur active element
2852
- if (e.key === 'Escape' && document.activeElement) {
2853
- document.activeElement.blur();
2873
+ // Escape: Close quick-add modal or blur active element
2874
+ if (e.key === 'Escape') {
2875
+ const overlay = $('quickAddOverlay');
2876
+ if (overlay && overlay.style.display !== 'none') {
2877
+ closeQuickAdd();
2878
+ return;
2879
+ }
2880
+ if (document.activeElement) document.activeElement.blur();
2854
2881
  }
2855
2882
  });
2856
2883
 
@@ -2890,7 +2917,6 @@
2890
2917
  window.refreshAnomalies = refreshAnomalies;
2891
2918
  window.refreshRiskRadar = refreshRiskRadar;
2892
2919
  window.refreshCompanionDash = refreshCompanionDash;
2893
- window.filterCompanionView = filterCompanionView;
2894
2920
  window.copyOut = copyOut;
2895
2921
  window.copyPath = copyPath;
2896
2922
  window.openSelected = openSelected;
@@ -2907,4 +2933,282 @@
2907
2933
  window.askFreya = askFreya;
2908
2934
  window.askFreyaInline = askFreyaInline;
2909
2935
  window.askFreyaFromInput = askFreyaFromInput;
2936
+
2937
+ /* ── Quick-Add Modal ── */
2938
+ function openQuickAdd() {
2939
+ const overlay = $('quickAddOverlay');
2940
+ if (!overlay) return;
2941
+ overlay.style.display = 'flex';
2942
+ const desc = $('qaDesc');
2943
+ if (desc) { desc.value = ''; desc.focus(); }
2944
+ var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
2945
+ var pri = $('qaPriority'); if (pri) pri.value = '';
2946
+ var slug = $('qaSlug'); if (slug) slug.value = '';
2947
+ var due = $('qaDue'); if (due) due.value = '';
2948
+ }
2949
+
2950
+ function closeQuickAdd() {
2951
+ var overlay = $('quickAddOverlay');
2952
+ if (overlay) overlay.style.display = 'none';
2953
+ }
2954
+
2955
+ async function submitQuickAdd() {
2956
+ var desc = $('qaDesc');
2957
+ var text = desc ? desc.value.trim() : '';
2958
+ if (!text) { showToast('err', 'Descricao obrigatoria'); return; }
2959
+
2960
+ var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
2961
+ var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
2962
+ var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
2963
+ var due = $('qaDue'); var dueVal = due ? due.value : '';
2964
+
2965
+ var body = { dir: dirOrDefault(), description: text, category: catVal };
2966
+ if (priVal) body.priority = priVal;
2967
+ if (slugVal) body.projectSlug = slugVal;
2968
+ if (dueVal) body.dueDate = dueVal;
2969
+
2970
+ try {
2971
+ await api('/api/tasks/create', body);
2972
+ closeQuickAdd();
2973
+ showToast('ok', 'Task criada');
2974
+ if (isKanbanPage) await loadKanban();
2975
+ else await refreshToday();
2976
+ } catch (e) {
2977
+ showToast('err', 'Erro ao criar task');
2978
+ }
2979
+ }
2980
+
2981
+ window.openQuickAdd = openQuickAdd;
2982
+ window.closeQuickAdd = closeQuickAdd;
2983
+ window.submitQuickAdd = submitQuickAdd;
2984
+
2985
+ /* ── Delta Banner ── */
2986
+ async function loadDelta() {
2987
+ var el = $('kanbanDelta') || $('deltaBanner');
2988
+ if (!el) return;
2989
+ try {
2990
+ var res = await api('/api/summary/delta', { dir: dirOrDefault() });
2991
+ if (!res || !res.delta) { el.style.display = 'none'; return; }
2992
+ var d = res.delta;
2993
+ var parts = [];
2994
+ if (d.completedTasks > 0) parts.push(d.completedTasks + ' concluida(s)');
2995
+ if (d.resolvedBlockers > 0) parts.push(d.resolvedBlockers + ' blocker(s) resolvido(s)');
2996
+ if (d.newTasks > 0) parts.push(d.newTasks + ' nova(s)');
2997
+ if (d.newBlockers > 0) parts.push(d.newBlockers + ' novo(s) blocker(s)');
2998
+ if (d.overdueTasks > 0) parts.push(d.overdueTasks + ' atrasada(s)');
2999
+ if (parts.length === 0) { el.style.display = 'none'; return; }
3000
+ el.style.display = 'flex';
3001
+ el.className = 'delta-banner';
3002
+ el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>'
3003
+ + '<span>Ultimas 24h: ' + escapeHtml(parts.join(' \u00b7 ')) + '</span>';
3004
+ } catch { el.style.display = 'none'; }
3005
+ }
3006
+
3007
+ /* ── Kanban Board ── */
3008
+ var _kanbanData = { tasks: [], blockers: [] };
3009
+
3010
+ async function loadKanban() {
3011
+ try {
3012
+ var res = await api('/api/tasks/kanban', { dir: dirOrDefault() });
3013
+ if (!res || !res.ok) return;
3014
+ _kanbanData.tasks = res.tasks || [];
3015
+ _kanbanData.blockers = res.blockers || [];
3016
+ populateKanbanProjects();
3017
+ renderKanban();
3018
+ renderKanbanBlockers();
3019
+ loadDelta();
3020
+ } catch (e) {
3021
+ showToast('err', 'Erro ao carregar kanban');
3022
+ }
3023
+ }
3024
+
3025
+ function populateKanbanProjects() {
3026
+ var sel = $('kanbanFilterProject');
3027
+ if (!sel) return;
3028
+ var slugs = new Set();
3029
+ _kanbanData.tasks.forEach(function(t) { if (t.projectSlug) slugs.add(t.projectSlug); });
3030
+ _kanbanData.blockers.forEach(function(b) { if (b.projectSlug) slugs.add(b.projectSlug); });
3031
+ var current = sel.value;
3032
+ sel.innerHTML = '<option value="">Todos os projetos</option>';
3033
+ Array.from(slugs).sort().forEach(function(s) {
3034
+ var opt = document.createElement('option');
3035
+ opt.value = s; opt.textContent = s;
3036
+ sel.appendChild(opt);
3037
+ });
3038
+ sel.value = current;
3039
+ }
3040
+
3041
+ function filterKanban() { renderKanban(); renderKanbanBlockers(); }
3042
+
3043
+ function getFilteredTasks() {
3044
+ var sel = $('kanbanFilterProject');
3045
+ var filter = sel ? sel.value : '';
3046
+ if (!filter) return _kanbanData.tasks;
3047
+ return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
3048
+ }
3049
+
3050
+ function renderKanban() {
3051
+ var tasks = getFilteredTasks();
3052
+ var today = new Date().toISOString().slice(0, 10);
3053
+ var sevenAgo = new Date(Date.now() - 7 * 86400000).toISOString();
3054
+
3055
+ var cols = {
3056
+ DO_NOW: [], SCHEDULE: [], DELEGATE: [], COMPLETED: []
3057
+ };
3058
+
3059
+ tasks.forEach(function(t) {
3060
+ if (t.status === 'COMPLETED') {
3061
+ if (t.completedAt && t.completedAt >= sevenAgo) cols.COMPLETED.push(t);
3062
+ } else if (t.status === 'PENDING' && cols[t.category]) {
3063
+ cols[t.category].push(t);
3064
+ }
3065
+ });
3066
+
3067
+ var idMap = { DO_NOW: 'colDoNow', SCHEDULE: 'colSchedule', DELEGATE: 'colDelegate', COMPLETED: 'colDone' };
3068
+ var countMap = { DO_NOW: 'countDoNow', SCHEDULE: 'countSchedule', DELEGATE: 'countDelegate', COMPLETED: 'countDone' };
3069
+
3070
+ Object.keys(cols).forEach(function(cat) {
3071
+ var el = $(idMap[cat]);
3072
+ var countEl = $(countMap[cat]);
3073
+ if (countEl) countEl.textContent = cols[cat].length;
3074
+ if (!el) return;
3075
+ el.innerHTML = '';
3076
+
3077
+ cols[cat].forEach(function(t) {
3078
+ var card = document.createElement('div');
3079
+ card.className = 'kanban-card';
3080
+ card.draggable = (cat !== 'COMPLETED');
3081
+ card.dataset.taskId = t.id;
3082
+ card.dataset.category = cat;
3083
+
3084
+ var pc = priColor(t.priority);
3085
+ var isOverdue = t.dueDate && t.dueDate < today && t.status === 'PENDING';
3086
+
3087
+ var html = '<div class="kanban-card-header">'
3088
+ + '<span class="kanban-pri-dot" style="background:' + pc.dot + ';" title="' + escapeHtml(pc.label) + '"></span>'
3089
+ + '<span class="kanban-card-desc">' + escapeHtml(t.description || '') + '</span>'
3090
+ + '</div>';
3091
+
3092
+ var meta = [];
3093
+ if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
3094
+ if (t.dueDate) {
3095
+ var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
3096
+ meta.push('<span class="' + dueCls + '">' + escapeHtml(t.dueDate) + '</span>');
3097
+ }
3098
+ if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
3099
+
3100
+ if (cat !== 'COMPLETED') {
3101
+ html += '<div class="kanban-card-actions">'
3102
+ + '<button class="kanban-action-btn complete-btn" title="Concluir">\u2713</button>'
3103
+ + '<button class="kanban-action-btn edit-btn" title="Editar">\u270E</button>'
3104
+ + '</div>';
3105
+ }
3106
+
3107
+ card.innerHTML = html;
3108
+
3109
+ // Drag events
3110
+ if (cat !== 'COMPLETED') {
3111
+ card.addEventListener('dragstart', function(e) {
3112
+ e.dataTransfer.setData('text/plain', t.id);
3113
+ e.dataTransfer.effectAllowed = 'move';
3114
+ card.classList.add('dragging');
3115
+ });
3116
+ card.addEventListener('dragend', function() {
3117
+ card.classList.remove('dragging');
3118
+ });
3119
+
3120
+ // Complete button
3121
+ var completeBtn = card.querySelector('.complete-btn');
3122
+ if (completeBtn) {
3123
+ completeBtn.onclick = async function() {
3124
+ try {
3125
+ await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
3126
+ showToast('ok', 'Concluida');
3127
+ await loadKanban();
3128
+ } catch { showToast('err', 'Falhou'); }
3129
+ };
3130
+ }
3131
+
3132
+ // Edit button - inline edit via prompt
3133
+ var editBtn = card.querySelector('.edit-btn');
3134
+ if (editBtn) {
3135
+ editBtn.onclick = async function() {
3136
+ var newCat = prompt('Categoria (DO_NOW|SCHEDULE|DELEGATE):', cat);
3137
+ if (!newCat) return;
3138
+ var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
3139
+ if (newSlug === null) return;
3140
+ var newDue = prompt('Due date (YYYY-MM-DD):', t.dueDate || '');
3141
+ if (newDue === null) return;
3142
+ try {
3143
+ await api('/api/tasks/update', {
3144
+ dir: dirOrDefault(), id: t.id,
3145
+ patch: { category: newCat, projectSlug: newSlug, dueDate: newDue || null }
3146
+ });
3147
+ showToast('ok', 'Atualizada');
3148
+ await loadKanban();
3149
+ } catch { showToast('err', 'Falhou'); }
3150
+ };
3151
+ }
3152
+ }
3153
+
3154
+ el.appendChild(card);
3155
+ });
3156
+
3157
+ // Drop zone
3158
+ if (cat !== 'COMPLETED') {
3159
+ el.addEventListener('dragover', function(e) {
3160
+ e.preventDefault();
3161
+ e.dataTransfer.dropEffect = 'move';
3162
+ el.classList.add('drag-over');
3163
+ });
3164
+ el.addEventListener('dragleave', function() {
3165
+ el.classList.remove('drag-over');
3166
+ });
3167
+ el.addEventListener('drop', async function(e) {
3168
+ e.preventDefault();
3169
+ el.classList.remove('drag-over');
3170
+ var taskId = e.dataTransfer.getData('text/plain');
3171
+ if (!taskId) return;
3172
+ try {
3173
+ await api('/api/tasks/update', {
3174
+ dir: dirOrDefault(), id: taskId,
3175
+ patch: { category: cat }
3176
+ });
3177
+ showToast('ok', 'Movida para ' + cat);
3178
+ await loadKanban();
3179
+ } catch { showToast('err', 'Falhou'); }
3180
+ });
3181
+ }
3182
+ });
3183
+ }
3184
+
3185
+ function renderKanbanBlockers() {
3186
+ var wrap = $('kanbanBlockers');
3187
+ var list = $('kanbanBlockersList');
3188
+ if (!wrap || !list) return;
3189
+
3190
+ var sel = $('kanbanFilterProject');
3191
+ var filter = sel ? sel.value : '';
3192
+ var blockers = _kanbanData.blockers;
3193
+ if (filter) blockers = blockers.filter(function(b) { return b.projectSlug === filter; });
3194
+
3195
+ if (blockers.length === 0) { wrap.style.display = 'none'; return; }
3196
+ wrap.style.display = 'block';
3197
+ list.innerHTML = '';
3198
+
3199
+ blockers.forEach(function(b) {
3200
+ var card = document.createElement('div');
3201
+ card.className = 'kanban-blocker-card';
3202
+ var color = sevColor(b.severity);
3203
+ card.innerHTML = '<span class="kanban-blocker-sev" style="background:' + color + '22; color:' + color + '; border-color:' + color + '44;">' + escapeHtml(b.severity || 'BLOCKER') + '</span>'
3204
+ + '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
3205
+ + (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
3206
+ + (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
3207
+ list.appendChild(card);
3208
+ });
3209
+ }
3210
+
3211
+ window.loadKanban = loadKanban;
3212
+ window.filterKanban = filterKanban;
3213
+ window.loadDelta = loadDelta;
2910
3214
  })();