@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.js CHANGED
@@ -624,7 +624,7 @@ function safeJson(res, code, obj) {
624
624
  function looksEmptyWorkspace(dir) {
625
625
  try {
626
626
  if (!exists(dir)) return true;
627
- const entries = fs.readdirSync(dir).filter((n) => !['.debuglogs', '.DS_Store'].includes(n));
627
+ const entries = fs.readdirSync(dir).filter((n) => !['.debuglogs', '.DS_Store', 'data', 'logs'].includes(n));
628
628
  return entries.length === 0;
629
629
  } catch {
630
630
  return true;
@@ -983,6 +983,11 @@ function docsHtml(defaultDir) {
983
983
  return buildDocsHtml(safeDefault, APP_VERSION);
984
984
  }
985
985
 
986
+ function kanbanHtml(defaultDir) {
987
+ const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
988
+ return buildKanbanHtml(safeDefault, APP_VERSION);
989
+ }
990
+
986
991
  function buildDocsHtml(safeDefault, appVersion) {
987
992
  const safeVersion = escapeHtml(appVersion || 'unknown');
988
993
  return `<!doctype html>
@@ -1035,6 +1040,9 @@ function buildDocsHtml(safeDefault, appVersion) {
1035
1040
  <button class="railBtn" id="railCompanion" type="button" title="Companion">
1036
1041
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1037
1042
  </button>
1043
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
1044
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1045
+ </button>
1038
1046
  <button class="railBtn" id="railProjects" type="button" title="Projects">
1039
1047
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1040
1048
  </button>
@@ -1174,6 +1182,9 @@ function buildHtml(safeDefault, appVersion) {
1174
1182
  <button class="railBtn" id="railCompanion" type="button" title="Companion">
1175
1183
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1176
1184
  </button>
1185
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
1186
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1187
+ </button>
1177
1188
  <button class="railBtn" id="railProjects" type="button" title="Projects">
1178
1189
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1179
1190
  </button>
@@ -1209,7 +1220,7 @@ function buildHtml(safeDefault, appVersion) {
1209
1220
  <div class="centerBody">
1210
1221
  <!-- Unified Input Panel -->
1211
1222
  <div class="promptShell">
1212
- <div class="promptBar" style="border-radius: 16px; display: flex; flex-direction: column; overflow: hidden;">
1223
+ <div class="promptBar" style="border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; flex: 1;">
1213
1224
  <!-- Header -->
1214
1225
  <div class="promptMeta" style="flex-shrink:0;">
1215
1226
  <div class="promptTitle" style="display: flex; align-items: center; gap: 8px;">
@@ -1223,7 +1234,7 @@ function buildHtml(safeDefault, appVersion) {
1223
1234
  </div>
1224
1235
 
1225
1236
  <!-- Textarea -->
1226
- <textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya.&#10;&#10;▸ Salvar & Processar → extrai tarefas e blockers do texto&#10;▸ Perguntar → consulta o histórico via busca semântica (RAG)" style="resize:none; min-height: 160px; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid var(--border); padding: 14px 16px; font-size: 13px; line-height: 1.6;"
1237
+ <textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya.&#10;&#10;▸ Salvar & Processar → extrai tarefas e blockers do texto&#10;▸ Perguntar → consulta o histórico via busca semântica (RAG)" style="resize:none; min-height: 200px; flex: 1; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid var(--border); padding: 14px 16px; font-size: 13px; line-height: 1.6;"
1227
1238
  onkeydown="if((event.metaKey||event.ctrlKey)&&event.key==='Enter'){event.preventDefault();window.saveAndPlan();}"></textarea>
1228
1239
 
1229
1240
  <!-- Actions bar -->
@@ -1257,7 +1268,7 @@ function buildHtml(safeDefault, appVersion) {
1257
1268
  </div>
1258
1269
 
1259
1270
  <!-- Chat thread: responses appear here after actions -->
1260
- <div id="chatThread" style="max-height: 280px; overflow-y:auto; overflow-x:hidden; padding:0 12px; display:flex; flex-direction:column; gap:8px; border-top: 1px solid var(--border);"></div>
1271
+ <div id="chatThread" style="flex: 0 1 auto; min-height: 0; max-height: 40vh; overflow-y:auto; overflow-x:hidden; padding:0 12px; display:none; flex-direction:column; gap:8px; border-top: 1px solid var(--border);"></div>
1261
1272
  </div>
1262
1273
  </div>
1263
1274
 
@@ -1317,6 +1328,39 @@ function buildHtml(safeDefault, appVersion) {
1317
1328
  </div>
1318
1329
  </div>
1319
1330
 
1331
+ <!-- Quick-add modal (Ctrl+K) -->
1332
+ <div id="quickAddOverlay" class="qa-overlay" style="display:none;">
1333
+ <div class="qa-modal">
1334
+ <div class="qa-header">
1335
+ <span style="font-weight:700; font-size:14px;">Nova Task</span>
1336
+ <button class="btn small" type="button" onclick="window.closeQuickAdd()" style="padding:2px 8px;">&times;</button>
1337
+ </div>
1338
+ <textarea id="qaDesc" class="qa-input" placeholder="Descri\u00e7\u00e3o da task..." rows="3"></textarea>
1339
+ <div class="qa-row">
1340
+ <select id="qaCat" class="qa-select">
1341
+ <option value="DO_NOW">DO_NOW</option>
1342
+ <option value="SCHEDULE">SCHEDULE</option>
1343
+ <option value="DELEGATE">DELEGATE</option>
1344
+ </select>
1345
+ <select id="qaPriority" class="qa-select">
1346
+ <option value="">Prioridade</option>
1347
+ <option value="critical">Cr\u00edtica</option>
1348
+ <option value="high">Alta</option>
1349
+ <option value="medium">M\u00e9dia</option>
1350
+ <option value="low">Baixa</option>
1351
+ </select>
1352
+ </div>
1353
+ <div class="qa-row">
1354
+ <input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
1355
+ <input id="qaDue" type="date" class="qa-input" style="width:160px;" />
1356
+ </div>
1357
+ <div class="qa-row" style="justify-content:flex-end;">
1358
+ <button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
1359
+ <button class="btn primary small" type="button" onclick="window.submitQuickAdd()">Criar Task</button>
1360
+ </div>
1361
+ </div>
1362
+ </div>
1363
+
1320
1364
  <script>
1321
1365
  window.__FREYA_DEFAULT_DIR = "${safeDefault}";
1322
1366
  </script>
@@ -1354,6 +1398,9 @@ function buildReportsHtml(safeDefault, appVersion) {
1354
1398
  <button class="railBtn" id="railCompanion" type="button" title="Companion">
1355
1399
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1356
1400
  </button>
1401
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
1402
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1403
+ </button>
1357
1404
  <button class="railBtn" id="railProjects" type="button" title="Projects">
1358
1405
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1359
1406
  </button>
@@ -1467,6 +1514,9 @@ function buildProjectsHtml(safeDefault, appVersion) {
1467
1514
  <button class="railBtn" id="railCompanion" type="button" title="Companion">
1468
1515
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1469
1516
  </button>
1517
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
1518
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1519
+ </button>
1470
1520
  <button class="railBtn active" id="railProjects" type="button" title="Projects">
1471
1521
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1472
1522
  </button>
@@ -1560,6 +1610,9 @@ function buildTimelineHtml(safeDefault, appVersion) {
1560
1610
  <button class=\"railBtn\" id=\"railCompanion\" type=\"button\" title=\"Companion\">
1561
1611
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1562
1612
  </button>
1613
+ <button class=\"railBtn\" id=\"railKanban\" type=\"button\" title=\"Kanban\">
1614
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1615
+ </button>
1563
1616
  <button class=\"railBtn\" id=\"railProjects\" type=\"button\" title=\"Projects\">
1564
1617
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1565
1618
  </button>
@@ -1669,6 +1722,9 @@ function buildGraphHtml(safeDefault, appVersion) {
1669
1722
  <button class="railBtn" id="railCompanion" type="button" title="Companion">
1670
1723
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1671
1724
  </button>
1725
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
1726
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1727
+ </button>
1672
1728
  <button class="railBtn" id="railProjects" type="button" title="Projects">
1673
1729
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1674
1730
  </button>
@@ -1760,6 +1816,9 @@ function buildCompanionHtml(safeDefault, appVersion) {
1760
1816
  <button class="railBtn active" id="railCompanion" type="button" title="Companion">
1761
1817
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
1762
1818
  </button>
1819
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
1820
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
1821
+ </button>
1763
1822
  <button class="railBtn" id="railProjects" type="button" title="Projects">
1764
1823
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
1765
1824
  </button>
@@ -1797,40 +1856,55 @@ function buildCompanionHtml(safeDefault, appVersion) {
1797
1856
  <section class="reportsHeader">
1798
1857
  <div>
1799
1858
  <div class="reportsTitle">Scrum Master Companion</div>
1800
- <div class="reportsSubtitle">Sprint Health Dashboard Visão consolidada de seus projetos</div>
1859
+ <div class="reportsSubtitle">Acompanhamento de projetos e frentes em tempo real.</div>
1801
1860
  </div>
1802
1861
  <div class="reportsActions">
1803
- <button class="btn small" type="button" onclick="refreshCompanionDash()">↻ Atualizar</button>
1862
+ <button class="btn small" type="button" onclick="refreshCompanionDash()">Atualizar Dashboard</button>
1804
1863
  </div>
1805
1864
  </section>
1806
1865
 
1807
- <!-- TAB FILTERS -->
1866
+ <button class="btn small" type="button" onclick="window.location.href='/reports'" style="margin-bottom: 16px;">
1867
+ 📊 Ver / Gerar Relatórios →
1868
+ </button>
1869
+
1870
+ <!-- PROJECTS OVERVIEW -->
1808
1871
  <div class="companionTabs">
1809
- <button class="tab active" data-filter="all" onclick="filterCompanionView('all')">○ Todos</button>
1810
- <button class="tab" data-filter="alerts" onclick="filterCompanionView('alerts')">⚠️ Alertas</button>
1811
- <button class="tab" data-filter="risk" onclick="filterCompanionView('risk')">🔴 Risco Alto</button>
1872
+ <button class="tab active" data-filter="all" onclick="window.filterCompanionView('all')">○ Todos</button>
1873
+ <button class="tab" data-filter="alerts" onclick="window.filterCompanionView('alerts')">● Alertas</button>
1874
+ <button class="tab" data-filter="risk" onclick="window.filterCompanionView('risk')">⚠ Risco Alto</button>
1812
1875
  </div>
1813
1876
 
1814
1877
  <!-- CONSOLIDATED VIEW -->
1815
- <div id="companionConsolidated" class="consolidatedView"></div>
1878
+ <div id="consolidatedViewBox" style="display: none;">
1879
+ <section class="panel">
1880
+ <div class="panelHead"><b>Visão Consolidada</b></div>
1881
+ <div class="panelBody">
1882
+ <div class="consolidatedView" id="consolidatedView" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;"></div>
1883
+ </div>
1884
+ </section>
1885
+ </div>
1816
1886
 
1817
1887
  <!-- PROJECT CARDS GRID -->
1818
- <div id="companionProjects" class="projectCardsGrid"></div>
1888
+ <div id="projectCardsBox" style="display: none;">
1889
+ <div class="projectCardsGrid" id="projectCardsGrid"></div>
1890
+ </div>
1819
1891
 
1820
1892
  <!-- STREAM BREAKDOWN -->
1821
- <section class="panel" style="margin-top: 24px;">
1822
- <div class="panelHead"><b>Distribuição por Frente/Stream</b></div>
1823
- <div class="panelBody">
1824
- <div id="streamBreakdown" class="help">Carregando streams...</div>
1825
- </div>
1826
- </section>
1893
+ <div id="streamBreakdownBox" style="display: none;">
1894
+ <section class="panel">
1895
+ <div class="panelHead"><b>Detalhamento por Stream</b></div>
1896
+ <div class="panelBody">
1897
+ <div id="streamBreakdown"></div>
1898
+ </div>
1899
+ </section>
1900
+ </div>
1827
1901
 
1828
- <!-- ALERTS ZONE -->
1829
- <div id="companionAlerts" style="display: none; margin-top: 24px;">
1902
+ <!-- ALERTS VIEW -->
1903
+ <div id="alertsViewBox" style="display: none;">
1830
1904
  <section class="panel">
1831
- <div class="panelHead"><b>⚠️ Alertas Prioritários</b></div>
1905
+ <div class="panelHead"><b>Alertas Prioritários</b></div>
1832
1906
  <div class="panelBody">
1833
- <div id="alertsList"></div>
1907
+ <div id="alertsView"></div>
1834
1908
  </div>
1835
1909
  </section>
1836
1910
  </div>
@@ -1998,18 +2072,6 @@ function truncateText(text, maxLen) {
1998
2072
  return str.slice(0, Math.max(0, maxLen - 3)).trimEnd() + '...';
1999
2073
  }
2000
2074
 
2001
- function formatAgo(timestampMs) {
2002
- const ms = typeof timestampMs === 'number' ? timestampMs : (typeof timestampMs === 'string' ? Date.parse(timestampMs) : 0);
2003
- if (!Number.isFinite(ms)) return 'unknown';
2004
- const now = Date.now();
2005
- const diffMs = now - ms;
2006
- if (diffMs < 60000) return 'agora';
2007
- if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m`;
2008
- if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h`;
2009
- if (diffMs < 604800000) return `${Math.floor(diffMs / 86400000)}d`;
2010
- return `${Math.floor(diffMs / 604800000)}w`;
2011
- }
2012
-
2013
2075
  function getTimelineItems(workspaceDir) {
2014
2076
  const items = [];
2015
2077
  const dailyDir = path.join(workspaceDir, 'logs', 'daily');
@@ -2319,6 +2381,14 @@ async function cmdWeb({ port, dir, open, dev }) {
2319
2381
  return;
2320
2382
  }
2321
2383
 
2384
+ if (req.method === 'GET' && req.url === '/kanban') {
2385
+ try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2386
+ const body = injectPort(kanbanHtml(dir || './freya'));
2387
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2388
+ res.end(body);
2389
+ return;
2390
+ }
2391
+
2322
2392
  if (req.method === 'GET' && req.url === '/app.css') {
2323
2393
  const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
2324
2394
  res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
@@ -2807,6 +2877,310 @@ async function cmdWeb({ port, dir, open, dev }) {
2807
2877
  return safeJson(res, 200, { ok: true, items });
2808
2878
  }
2809
2879
 
2880
+ // /api/companion/projects-summary: Consolidate project KPIs
2881
+ if (req.url === '/api/companion/projects-summary') {
2882
+ if (!looksLikeFreyaWorkspace(workspaceDir)) {
2883
+ return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
2884
+ }
2885
+
2886
+ const now = Date.now();
2887
+ const projectMap = {}; // projectSlug -> { name, totalTasks, completedTasks, pendingTasks, blockers, streams, lastUpdateAgo, status }
2888
+
2889
+ // 1. Load tasks
2890
+ const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2891
+ if (exists(taskFile)) {
2892
+ const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
2893
+ const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
2894
+
2895
+ for (const t of tasks) {
2896
+ const slug = String(t.projectSlug || '').trim();
2897
+ if (!slug) continue;
2898
+
2899
+ if (!projectMap[slug]) {
2900
+ projectMap[slug] = {
2901
+ slug, name: slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
2902
+ openBlockers: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
2903
+ streams: new Set(), lastUpdateMs: now, status: 'ON_TRACK'
2904
+ };
2905
+ }
2906
+
2907
+ projectMap[slug].totalTasks++;
2908
+ if (t.status === 'COMPLETED') {
2909
+ projectMap[slug].completedTasks++;
2910
+ } else {
2911
+ projectMap[slug].pendingTasks++;
2912
+ }
2913
+
2914
+ const streamSlug = String(t.streamSlug || '').trim();
2915
+ if (streamSlug) {
2916
+ projectMap[slug].streams.add(streamSlug);
2917
+ }
2918
+
2919
+ if (t.createdAt) {
2920
+ const taskTime = Date.parse(t.createdAt);
2921
+ if (taskTime > projectMap[slug].lastUpdateMs) {
2922
+ projectMap[slug].lastUpdateMs = taskTime;
2923
+ }
2924
+ }
2925
+ }
2926
+ }
2927
+
2928
+ // 2. Load blockers
2929
+ const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
2930
+ if (exists(blockerFile)) {
2931
+ const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
2932
+ const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
2933
+
2934
+ for (const b of blockers) {
2935
+ const slug = String(b.projectSlug || '').trim();
2936
+ if (!slug) continue;
2937
+
2938
+ if (!projectMap[slug]) {
2939
+ projectMap[slug] = {
2940
+ slug, name: slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
2941
+ openBlockers: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
2942
+ streams: new Set(), lastUpdateMs: now, status: 'ON_TRACK'
2943
+ };
2944
+ }
2945
+
2946
+ if (String(b.status || '').toUpperCase() === 'OPEN') {
2947
+ projectMap[slug].openBlockers++;
2948
+ const sev = String(b.severity || 'LOW').toUpperCase();
2949
+ if (projectMap[slug].blockersBySeverity[sev] !== undefined) {
2950
+ projectMap[slug].blockersBySeverity[sev]++;
2951
+ }
2952
+ }
2953
+
2954
+ if (b.createdAt) {
2955
+ const blockerTime = Date.parse(b.createdAt);
2956
+ if (blockerTime > projectMap[slug].lastUpdateMs) {
2957
+ projectMap[slug].lastUpdateMs = blockerTime;
2958
+ }
2959
+ }
2960
+ }
2961
+ }
2962
+
2963
+ // Helper: format time ago
2964
+ const formatAgo = (ms) => {
2965
+ const age = now - ms;
2966
+ const secs = Math.floor(age / 1000);
2967
+ if (secs < 60) return 'just now';
2968
+ const mins = Math.floor(secs / 60);
2969
+ if (mins < 60) return mins + 'm ago';
2970
+ const hours = Math.floor(mins / 60);
2971
+ if (hours < 24) return hours + 'h ago';
2972
+ const days = Math.floor(hours / 24);
2973
+ return days + 'd ago';
2974
+ };
2975
+
2976
+ // Calculate status and completion rate for each project
2977
+ const projects = [];
2978
+ for (const [slug, proj] of Object.entries(projectMap)) {
2979
+ const completionRate = proj.totalTasks > 0 ? Math.round((proj.completedTasks / proj.totalTasks) * 100) : 0;
2980
+ let status = 'ON_TRACK';
2981
+
2982
+ if (proj.blockersBySeverity.CRITICAL > 0) status = 'AT_RISK';
2983
+ if (proj.blockersBySeverity.HIGH >= 3) status = 'AT_RISK';
2984
+ if (proj.pendingTasks > 15) status = 'AT_RISK';
2985
+
2986
+ const ageDays = Math.floor((now - proj.lastUpdateMs) / (24 * 60 * 60 * 1000));
2987
+ if (ageDays > 14) status = 'IDLE';
2988
+
2989
+ projects.push({
2990
+ slug: proj.slug,
2991
+ name: proj.name,
2992
+ totalTasks: proj.totalTasks,
2993
+ completedTasks: proj.completedTasks,
2994
+ pendingTasks: proj.pendingTasks,
2995
+ completionRate,
2996
+ openBlockers: proj.openBlockers,
2997
+ blockersBySeverity: proj.blockersBySeverity,
2998
+ streams: Array.from(proj.streams),
2999
+ lastUpdateAgo: formatAgo(proj.lastUpdateMs),
3000
+ status
3001
+ });
3002
+ }
3003
+
3004
+ return safeJson(res, 200, { ok: true, projects });
3005
+ }
3006
+
3007
+ // /api/companion/streams-breakdown: Task/blocker breakdown by stream within each project
3008
+ if (req.url === '/api/companion/streams-breakdown') {
3009
+ if (!looksLikeFreyaWorkspace(workspaceDir)) {
3010
+ return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
3011
+ }
3012
+
3013
+ const breakdownMap = {}; // projectSlug -> { projectName, streams: [...] }
3014
+
3015
+ // 1. Load tasks grouped by project & stream
3016
+ const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
3017
+ if (exists(taskFile)) {
3018
+ const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
3019
+ const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
3020
+
3021
+ for (const t of tasks) {
3022
+ const pSlug = String(t.projectSlug || '').trim();
3023
+ const sSlug = String(t.streamSlug || 'default').trim();
3024
+ if (!pSlug) continue;
3025
+
3026
+ if (!breakdownMap[pSlug]) {
3027
+ breakdownMap[pSlug] = { projectName: pSlug, streams: {} };
3028
+ }
3029
+ if (!breakdownMap[pSlug].streams[sSlug]) {
3030
+ breakdownMap[pSlug].streams[sSlug] = {
3031
+ streamName: sSlug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
3032
+ blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
3033
+ };
3034
+ }
3035
+
3036
+ breakdownMap[pSlug].streams[sSlug].totalTasks++;
3037
+ if (t.status === 'COMPLETED') {
3038
+ breakdownMap[pSlug].streams[sSlug].completedTasks++;
3039
+ } else {
3040
+ breakdownMap[pSlug].streams[sSlug].pendingTasks++;
3041
+ }
3042
+ }
3043
+ }
3044
+
3045
+ // 2. Load blockers grouped by project & stream
3046
+ const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
3047
+ if (exists(blockerFile)) {
3048
+ const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
3049
+ const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
3050
+
3051
+ for (const b of blockers) {
3052
+ const pSlug = String(b.projectSlug || '').trim();
3053
+ const sSlug = String(b.streamSlug || 'default').trim();
3054
+ if (!pSlug) continue;
3055
+
3056
+ if (!breakdownMap[pSlug]) {
3057
+ breakdownMap[pSlug] = { projectName: pSlug, streams: {} };
3058
+ }
3059
+ if (!breakdownMap[pSlug].streams[sSlug]) {
3060
+ breakdownMap[pSlug].streams[sSlug] = {
3061
+ streamName: sSlug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
3062
+ blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
3063
+ };
3064
+ }
3065
+
3066
+ if (String(b.status || '').toUpperCase() === 'OPEN') {
3067
+ breakdownMap[pSlug].streams[sSlug].blockersCount++;
3068
+ const sev = String(b.severity || 'LOW').toUpperCase();
3069
+ if (breakdownMap[pSlug].streams[sSlug].blockersBySeverity[sev] !== undefined) {
3070
+ breakdownMap[pSlug].streams[sSlug].blockersBySeverity[sev]++;
3071
+ }
3072
+ }
3073
+ }
3074
+ }
3075
+
3076
+ // Convert to array format
3077
+ const breakdown = [];
3078
+ for (const [pSlug, pData] of Object.entries(breakdownMap)) {
3079
+ const streams = [];
3080
+ for (const [sSlug, sData] of Object.entries(pData.streams)) {
3081
+ streams.push({
3082
+ streamName: sData.streamName,
3083
+ totalTasks: sData.totalTasks,
3084
+ completedTasks: sData.completedTasks,
3085
+ pendingTasks: sData.pendingTasks,
3086
+ blockersCount: sData.blockersCount,
3087
+ blockersBySeverity: sData.blockersBySeverity
3088
+ });
3089
+ }
3090
+ breakdown.push({
3091
+ projectSlug: pSlug,
3092
+ projectName: pData.projectName,
3093
+ streams: streams.sort((a, b) => b.blockersCount - a.blockersCount)
3094
+ });
3095
+ }
3096
+
3097
+ return safeJson(res, 200, { ok: true, breakdown });
3098
+ }
3099
+
3100
+ // /api/companion/alerts: Prioritized alerts by severity
3101
+ if (req.url === '/api/companion/alerts') {
3102
+ if (!looksLikeFreyaWorkspace(workspaceDir)) {
3103
+ return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
3104
+ }
3105
+
3106
+ const now = Date.now();
3107
+ const alerts = [];
3108
+
3109
+ // 1. Check for old blockers
3110
+ const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
3111
+ if (exists(blockerFile)) {
3112
+ const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
3113
+ const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
3114
+
3115
+ for (const b of blockers) {
3116
+ if (String(b.status || '').toUpperCase() === 'OPEN') {
3117
+ const createdTime = b.createdAt ? Date.parse(b.createdAt) : now;
3118
+ const ageDays = Math.floor((now - createdTime) / (24 * 60 * 60 * 1000));
3119
+
3120
+ let severity = 'MEDIUM';
3121
+ if (String(b.severity || '').toUpperCase() === 'CRITICAL') severity = 'CRITICAL';
3122
+ else if (ageDays > 7) severity = 'HIGH';
3123
+
3124
+ alerts.push({
3125
+ severity,
3126
+ type: 'old_blocker',
3127
+ projectSlug: String(b.projectSlug || '').trim(),
3128
+ streamName: String(b.streamSlug || '').trim(),
3129
+ message: `Bloqueio: ${b.title} (${ageDays} dias)`,
3130
+ age: ageDays,
3131
+ createdAt: b.createdAt
3132
+ });
3133
+ }
3134
+ }
3135
+ }
3136
+
3137
+ // 2. Check for stale projects
3138
+ const base = path.join(workspaceDir, 'data', 'Clients');
3139
+ if (exists(base)) {
3140
+ const stack = [base];
3141
+ while (stack.length) {
3142
+ const dirp = stack.pop();
3143
+ const entries = fs.readdirSync(dirp, { withFileTypes: true });
3144
+ for (const ent of entries) {
3145
+ const full = path.join(dirp, ent.name);
3146
+ if (ent.isDirectory()) stack.push(full);
3147
+ else if (ent.isFile() && ent.name === 'status.json') {
3148
+ const doc = readJsonOrNull(full) || {};
3149
+ const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
3150
+ if (doc.active !== false) {
3151
+ const lastUpdated = doc.lastUpdated ? Date.parse(doc.lastUpdated) : null;
3152
+ if (lastUpdated) {
3153
+ const ageDays = Math.floor((now - lastUpdated) / (24 * 60 * 60 * 1000));
3154
+ if (ageDays > 14) {
3155
+ alerts.push({
3156
+ severity: ageDays > 30 ? 'CRITICAL' : 'HIGH',
3157
+ type: 'stale_project',
3158
+ projectSlug: slug,
3159
+ streamName: '',
3160
+ message: `Projeto inativo por ${ageDays} dias`,
3161
+ age: ageDays,
3162
+ createdAt: doc.lastUpdated
3163
+ });
3164
+ }
3165
+ }
3166
+ }
3167
+ }
3168
+ }
3169
+ }
3170
+ }
3171
+
3172
+ // Sort by severity (CRITICAL > HIGH > MEDIUM) then by age
3173
+ const severityOrder = { CRITICAL: 3, HIGH: 2, MEDIUM: 1 };
3174
+ alerts.sort((a, b) => {
3175
+ const sA = severityOrder[a.severity] || 0;
3176
+ const sB = severityOrder[b.severity] || 0;
3177
+ if (sA !== sB) return sB - sA;
3178
+ return (b.age || 0) - (a.age || 0);
3179
+ });
3180
+
3181
+ return safeJson(res, 200, { ok: true, alerts });
3182
+ }
3183
+
2810
3184
  if (req.url === '/api/incidents/resolve') {
2811
3185
  const title = payload.title;
2812
3186
  const index = Number.isInteger(payload.index) ? payload.index : null;
@@ -3417,15 +3791,19 @@ async function cmdWeb({ port, dir, open, dev }) {
3417
3791
  return `\n\n---\nFILE: ${rel}\n---\n` + fs.readFileSync(p, 'utf8');
3418
3792
  }).join('');
3419
3793
 
3420
- // V2 RAG Context
3794
+ // V2 RAG Context (graceful fallback if embedder/sharp not available)
3421
3795
  const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
3422
- const ragResults = await dm.semanticSearch(query, 12);
3423
3796
  let ragContext = '';
3424
- if (ragResults.length > 0) {
3425
- ragContext = '\n\n[MEMÓRIA DE LONGO PRAZO RECUPERADA (RAG VIA SQLITE)]\n';
3426
- for (const r of ragResults) {
3427
- ragContext += `\n---\nFONTE: ${r.reference_type} -> ID: ${r.reference_id} (Score: ${r.score.toFixed(3)})\nCONTEÚDO:\n${r.text_chunk}\n`;
3797
+ try {
3798
+ const ragResults = await dm.semanticSearch(query, 12);
3799
+ if (ragResults.length > 0) {
3800
+ ragContext = '\n\n[MEMÓRIA DE LONGO PRAZO RECUPERADA (RAG VIA SQLITE)]\n';
3801
+ for (const r of ragResults) {
3802
+ ragContext += `\n---\nFONTE: ${r.reference_type} -> ID: ${r.reference_id} (Score: ${r.score.toFixed(3)})\nCONTEÚDO:\n${r.text_chunk}\n`;
3803
+ }
3428
3804
  }
3805
+ } catch (ragErr) {
3806
+ console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
3429
3807
  }
3430
3808
 
3431
3809
  const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
@@ -3575,6 +3953,7 @@ async function cmdWeb({ port, dir, open, dev }) {
3575
3953
  status: t.status,
3576
3954
  createdAt: t.created_at,
3577
3955
  completedAt: t.completed_at,
3956
+ dueDate: t.due_date || null,
3578
3957
  projectSlug: t.project_slug,
3579
3958
  priority: meta.priority,
3580
3959
  streamSlug: meta.streamSlug,
@@ -3627,6 +4006,33 @@ async function cmdWeb({ port, dir, open, dev }) {
3627
4006
  queryUpdates.push('category = ?');
3628
4007
  params.push(patch.category.trim());
3629
4008
  }
4009
+ if (typeof patch.dueDate === 'string') {
4010
+ queryUpdates.push('due_date = ?');
4011
+ params.push(patch.dueDate.trim() || null);
4012
+ }
4013
+ if (patch.dueDate === null) {
4014
+ queryUpdates.push('due_date = NULL');
4015
+ }
4016
+ if (typeof patch.status === 'string') {
4017
+ queryUpdates.push('status = ?');
4018
+ params.push(patch.status.trim());
4019
+ if (patch.status === 'COMPLETED') {
4020
+ queryUpdates.push('completed_at = ?');
4021
+ params.push(new Date().toISOString());
4022
+ }
4023
+ }
4024
+ if (typeof patch.description === 'string') {
4025
+ queryUpdates.push('description = ?');
4026
+ params.push(patch.description.trim());
4027
+ }
4028
+ if (typeof patch.priority === 'string') {
4029
+ const row = dl.db.prepare('SELECT metadata FROM tasks WHERE id = ?').get(id);
4030
+ let meta = {};
4031
+ try { meta = row && row.metadata ? JSON.parse(row.metadata) : {}; } catch { meta = {}; }
4032
+ meta.priority = patch.priority.trim().toLowerCase();
4033
+ queryUpdates.push('metadata = ?');
4034
+ params.push(JSON.stringify(meta));
4035
+ }
3630
4036
 
3631
4037
  if (queryUpdates.length === 0) return safeJson(res, 200, { ok: true, task: { id } });
3632
4038
 
@@ -3666,202 +4072,6 @@ async function cmdWeb({ port, dir, open, dev }) {
3666
4072
  return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
3667
4073
  }
3668
4074
 
3669
- if (req.url === '/api/companion/projects-summary') {
3670
- if (!looksLikeFreyaWorkspace(workspaceDir)) {
3671
- return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', projects: [] });
3672
- }
3673
-
3674
- const now = Date.now();
3675
- const projects = {};
3676
-
3677
- // Aggregate tasks by projectSlug
3678
- const tasksRaw = dl.db.prepare('SELECT * FROM tasks WHERE status IN ("PENDING", "COMPLETED")').all();
3679
- const blockersRaw = dl.db.prepare('SELECT * FROM blockers WHERE status IN ("OPEN", "MITIGATING")').all();
3680
-
3681
- // Build project summary
3682
- for (const t of tasksRaw) {
3683
- const slug = String(t.project_slug || 'unassigned').trim();
3684
- if (!projects[slug]) {
3685
- projects[slug] = { slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0, streams: new Set(), blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }, oldestBlockerDays: null, lastUpdateMs: null };
3686
- }
3687
- projects[slug].totalTasks++;
3688
- if (String(t.status).toUpperCase() === 'COMPLETED') projects[slug].completedTasks++;
3689
- else projects[slug].pendingTasks++;
3690
- if (t.completed_at) projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, Date.parse(t.completed_at));
3691
- if (t.created_at) projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, Date.parse(t.created_at));
3692
- const meta = t.metadata ? JSON.parse(String(t.metadata || '{}')) : {};
3693
- if (meta.streamSlug) projects[slug].streams.add(String(meta.streamSlug));
3694
- }
3695
-
3696
- // Add blockers to projects
3697
- for (const b of blockersRaw) {
3698
- const slug = String(b.project_slug || 'unassigned').trim();
3699
- if (!projects[slug]) {
3700
- projects[slug] = { slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0, streams: new Set(), blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }, oldestBlockerDays: null, lastUpdateMs: null };
3701
- }
3702
- projects[slug].blockersCount++;
3703
- const sev = String(b.severity || 'LOW').toUpperCase();
3704
- if (projects[slug].blockersBySeverity[sev] !== undefined) projects[slug].blockersBySeverity[sev]++;
3705
- const createdMs = b.created_at ? Date.parse(b.created_at) : null;
3706
- if (createdMs) {
3707
- projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, createdMs);
3708
- const ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
3709
- if (projects[slug].oldestBlockerDays === null || ageDays > projects[slug].oldestBlockerDays) {
3710
- projects[slug].oldestBlockerDays = ageDays;
3711
- }
3712
- }
3713
- }
3714
-
3715
- // Convert to array and calculate status + velocity
3716
- const projectsArray = Object.values(projects).map(p => {
3717
- const completedThisWeek = dl.db.prepare('SELECT count(*) as count FROM tasks WHERE project_slug = ? AND status = "COMPLETED" AND completed_at > ?').get(p.slug, new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString());
3718
- const velocityThisWeek = completedThisWeek ? (completedThisWeek.count / 7).toFixed(1) : '0';
3719
- const lastUpdateAgo = p.lastUpdateMs ? formatAgo(p.lastUpdateMs) : 'nunca';
3720
- const hasOldBlockers = p.oldestBlockerDays && p.oldestBlockerDays > 7;
3721
- const hasHighSeverity = p.blockersBySeverity.CRITICAL > 0 || p.blockersBySeverity.HIGH > 0;
3722
- const isIdle = p.lastUpdateMs && (now - p.lastUpdateMs) > 7 * 24 * 60 * 60 * 1000;
3723
- const status = isIdle ? 'IDLE' : (hasHighSeverity || hasOldBlockers ? 'AT_RISK' : 'ON_TRACK');
3724
- return {
3725
- slug: p.slug,
3726
- name: p.slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
3727
- totalTasks: p.totalTasks,
3728
- completedTasks: p.completedTasks,
3729
- pendingTasks: p.pendingTasks,
3730
- blockersCount: p.blockersCount,
3731
- blockersBySeverity: p.blockersBySeverity,
3732
- oldestBlockerDays: p.oldestBlockerDays,
3733
- velocityThisWeek: parseFloat(velocityThisWeek),
3734
- lastUpdateAgo: lastUpdateAgo,
3735
- status: status,
3736
- streams: Array.from(p.streams)
3737
- };
3738
- });
3739
-
3740
- return safeJson(res, 200, { ok: true, projects: projectsArray });
3741
- }
3742
-
3743
- if (req.url === '/api/companion/streams-breakdown') {
3744
- if (!looksLikeFreyaWorkspace(workspaceDir)) {
3745
- return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', items: [] });
3746
- }
3747
-
3748
- const tasks = dl.db.prepare('SELECT * FROM tasks').all();
3749
- const blockers = dl.db.prepare('SELECT * FROM blockers').all();
3750
-
3751
- const breakdown = {};
3752
- for (const t of tasks) {
3753
- const projectSlug = String(t.project_slug || 'unassigned');
3754
- if (!breakdown[projectSlug]) breakdown[projectSlug] = {};
3755
- const meta = t.metadata ? JSON.parse(String(t.metadata || '{}')) : {};
3756
- const streamName = String(meta.streamSlug || 'Default');
3757
- if (!breakdown[projectSlug][streamName]) {
3758
- breakdown[projectSlug][streamName] = { totalTasks: 0, completedTasks: 0, pendingTasks: 0, blockersCount: 0, blockersHighestSeverity: null };
3759
- }
3760
- breakdown[projectSlug][streamName].totalTasks++;
3761
- if (String(t.status).toUpperCase() === 'COMPLETED') breakdown[projectSlug][streamName].completedTasks++;
3762
- else breakdown[projectSlug][streamName].pendingTasks++;
3763
- }
3764
-
3765
- for (const b of blockers) {
3766
- if (String(b.status).toUpperCase() !== 'OPEN' && String(b.status).toUpperCase() !== 'MITIGATING') continue;
3767
- const projectSlug = String(b.project_slug || 'unassigned');
3768
- if (!breakdown[projectSlug]) breakdown[projectSlug] = {};
3769
- const meta = b.metadata ? JSON.parse(String(b.metadata || '{}')) : {};
3770
- const streamName = String(meta.streamSlug || 'Default');
3771
- if (!breakdown[projectSlug][streamName]) {
3772
- breakdown[projectSlug][streamName] = { totalTasks: 0, completedTasks: 0, pendingTasks: 0, blockersCount: 0, blockersHighestSeverity: null };
3773
- }
3774
- breakdown[projectSlug][streamName].blockersCount++;
3775
- const sev = String(b.severity || 'LOW').toUpperCase();
3776
- const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
3777
- const sevVal = sevOrder[sev] !== undefined ? sevOrder[sev] : 999;
3778
- const currentVal = breakdown[projectSlug][streamName].blockersHighestSeverity ? sevOrder[breakdown[projectSlug][streamName].blockersHighestSeverity] : 999;
3779
- if (sevVal < currentVal) breakdown[projectSlug][streamName].blockersHighestSeverity = sev;
3780
- }
3781
-
3782
- const items = Object.entries(breakdown).map(([projectSlug, streams]) => ({
3783
- projectSlug,
3784
- projectName: projectSlug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
3785
- streams: Object.entries(streams).map(([streamName, data]) => ({ streamName, ...data }))
3786
- }));
3787
-
3788
- return safeJson(res, 200, { ok: true, items });
3789
- }
3790
-
3791
- if (req.url === '/api/companion/alerts') {
3792
- if (!looksLikeFreyaWorkspace(workspaceDir)) {
3793
- return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', alerts: [] });
3794
- }
3795
-
3796
- const now = Date.now();
3797
- const alerts = [];
3798
-
3799
- const blockers = dl.db.prepare('SELECT * FROM blockers WHERE status IN ("OPEN", "MITIGATING")').all();
3800
- const tasks = dl.db.prepare('SELECT * FROM tasks WHERE status = "PENDING"').all();
3801
-
3802
- // Old blockers
3803
- for (const b of blockers) {
3804
- const createdMs = b.created_at ? Date.parse(b.created_at) : null;
3805
- if (createdMs) {
3806
- const ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
3807
- if (ageDays > 3) {
3808
- const sev = String(b.severity || '').toUpperCase();
3809
- alerts.push({
3810
- severity: sev === 'CRITICAL' ? 'CRITICAL' : sev === 'HIGH' ? 'HIGH' : 'MEDIUM',
3811
- type: 'old_blocker',
3812
- projectSlug: String(b.project_slug || ''),
3813
- streamName: 'blocker: ' + (b.title || ''),
3814
- message: `Blocker aberto há ${ageDays} dias: ${b.title || 'Sem titulo'}`,
3815
- ageDays
3816
- });
3817
- }
3818
- }
3819
- }
3820
-
3821
- // Stale projects (7+ days without update)
3822
- const projects = {};
3823
- for (const t of tasks) {
3824
- const slug = String(t.project_slug || '');
3825
- if (slug) {
3826
- const createdMs = t.created_at ? Date.parse(t.created_at) : null;
3827
- if (createdMs && (!projects[slug] || createdMs > projects[slug])) projects[slug] = createdMs;
3828
- }
3829
- }
3830
- for (const b of blockers) {
3831
- const slug = String(b.project_slug || '');
3832
- if (slug) {
3833
- const createdMs = b.created_at ? Date.parse(b.created_at) : null;
3834
- if (createdMs && (!projects[slug] || createdMs > projects[slug])) projects[slug] = createdMs;
3835
- }
3836
- }
3837
- for (const [slug, lastMs] of Object.entries(projects)) {
3838
- if (lastMs) {
3839
- const ageDays = Math.floor((now - lastMs) / (24 * 60 * 60 * 1000));
3840
- if (ageDays > 7) {
3841
- alerts.push({
3842
- severity: ageDays > 14 ? 'HIGH' : 'MEDIUM',
3843
- type: 'stale_project',
3844
- projectSlug: slug,
3845
- streamName: '',
3846
- message: `Projeto ${slug} sem updates há ${ageDays} dias`,
3847
- ageDays
3848
- });
3849
- }
3850
- }
3851
- }
3852
-
3853
- // Sort by severity + age
3854
- alerts.sort((a, b) => {
3855
- const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2 };
3856
- const aVal = sevOrder[a.severity] || 99;
3857
- const bVal = sevOrder[b.severity] || 99;
3858
- if (aVal !== bVal) return aVal - bVal;
3859
- return (b.ageDays || 0) - (a.ageDays || 0);
3860
- });
3861
-
3862
- return safeJson(res, 200, { ok: true, alerts: alerts.slice(0, 10) });
3863
- }
3864
-
3865
4075
  // BUG-12: duplicate handlers for /api/incidents/resolve, /api/incidents,
3866
4076
  // and /api/tasks/heatmap were removed here — originals remain earlier in the file.
3867
4077
 
@@ -3974,6 +4184,100 @@ async function cmdWeb({ port, dir, open, dev }) {
3974
4184
  return safeJson(res, 200, { ok: true, blocker: { id, ...patch } });
3975
4185
  }
3976
4186
 
4187
+ // --- Quick-add: create a single task directly ---
4188
+ if (req.url === '/api/tasks/create') {
4189
+ const desc = String(payload.description || '').trim();
4190
+ if (!desc) return safeJson(res, 400, { error: 'Missing description' });
4191
+
4192
+ const cat = String(payload.category || 'DO_NOW').trim().toUpperCase();
4193
+ const validCats = new Set(['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE']);
4194
+ if (!validCats.has(cat)) return safeJson(res, 400, { error: 'Invalid category' });
4195
+
4196
+ const slug = typeof payload.projectSlug === 'string' ? payload.projectSlug.trim() || null : null;
4197
+ const dueDate = typeof payload.dueDate === 'string' ? payload.dueDate.trim() || null : null;
4198
+ const pri = typeof payload.priority === 'string' ? payload.priority.trim().toLowerCase() : undefined;
4199
+ const meta = {};
4200
+ if (pri) meta.priority = pri;
4201
+
4202
+ const id = `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
4203
+ dl.db.prepare('INSERT INTO tasks (id, project_slug, description, category, status, due_date, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)')
4204
+ .run(id, slug, desc, cat, 'PENDING', dueDate, Object.keys(meta).length ? JSON.stringify(meta) : null);
4205
+
4206
+ return safeJson(res, 200, { ok: true, task: { id, description: desc, category: cat, status: 'PENDING', projectSlug: slug, dueDate, priority: pri } });
4207
+ }
4208
+
4209
+ // --- Delta summary: what changed since yesterday ---
4210
+ if (req.url === '/api/summary/delta') {
4211
+ const now = new Date();
4212
+ const yesterday = new Date(now);
4213
+ yesterday.setDate(yesterday.getDate() - 1);
4214
+ const yIso = yesterday.toISOString();
4215
+
4216
+ const completed = dl.db.prepare("SELECT count(*) as c FROM tasks WHERE status = 'COMPLETED' AND completed_at >= ?").get(yIso);
4217
+ const newTasks = dl.db.prepare("SELECT count(*) as c FROM tasks WHERE created_at >= ?").get(yIso);
4218
+ const resolvedBlockers = dl.db.prepare("SELECT count(*) as c FROM blockers WHERE resolved_at >= ?").get(yIso);
4219
+ const newBlockers = dl.db.prepare("SELECT count(*) as c FROM blockers WHERE created_at >= ?").get(yIso);
4220
+ const overdue = dl.db.prepare("SELECT count(*) as c FROM tasks WHERE status = 'PENDING' AND due_date IS NOT NULL AND due_date < ?")
4221
+ .get(now.toISOString().slice(0, 10));
4222
+
4223
+ return safeJson(res, 200, {
4224
+ ok: true,
4225
+ delta: {
4226
+ completedTasks: completed ? completed.c : 0,
4227
+ newTasks: newTasks ? newTasks.c : 0,
4228
+ resolvedBlockers: resolvedBlockers ? resolvedBlockers.c : 0,
4229
+ newBlockers: newBlockers ? newBlockers.c : 0,
4230
+ overdueTasks: overdue ? overdue.c : 0
4231
+ }
4232
+ });
4233
+ }
4234
+
4235
+ // --- Kanban: all active tasks for board view ---
4236
+ if (req.url === '/api/tasks/kanban') {
4237
+ const rawTasks = dl.db.prepare(`
4238
+ SELECT * FROM tasks WHERE status != 'ARCHIVED'
4239
+ ORDER BY
4240
+ CASE JSON_EXTRACT(metadata, '$.priority')
4241
+ WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4
4242
+ END ASC,
4243
+ created_at DESC
4244
+ `).all();
4245
+
4246
+ const tasks = rawTasks.map(t => {
4247
+ let meta = {};
4248
+ try { meta = t.metadata ? JSON.parse(t.metadata) : {}; } catch { meta = {}; }
4249
+ return {
4250
+ id: t.id,
4251
+ description: t.description,
4252
+ category: t.category,
4253
+ status: t.status,
4254
+ createdAt: t.created_at,
4255
+ completedAt: t.completed_at,
4256
+ dueDate: t.due_date || null,
4257
+ projectSlug: t.project_slug,
4258
+ priority: meta.priority,
4259
+ streamSlug: meta.streamSlug
4260
+ };
4261
+ });
4262
+
4263
+ const openBlockers = dl.db.prepare(`
4264
+ SELECT * FROM blockers WHERE status IN ('OPEN', 'MITIGATING')
4265
+ ORDER BY
4266
+ CASE severity WHEN 'CRITICAL' THEN 0 WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 WHEN 'LOW' THEN 3 ELSE 9 END ASC,
4267
+ created_at ASC
4268
+ `).all().map(b => ({
4269
+ id: b.id,
4270
+ title: b.title,
4271
+ severity: b.severity,
4272
+ status: b.status,
4273
+ projectSlug: b.project_slug,
4274
+ owner: b.owner,
4275
+ createdAt: b.created_at
4276
+ }));
4277
+
4278
+ return safeJson(res, 200, { ok: true, tasks, blockers: openBlockers });
4279
+ }
4280
+
3977
4281
  if (req.url === '/api/report') {
3978
4282
  const script = payload.script;
3979
4283
  if (!script) return safeJson(res, 400, { error: 'Missing script' });
@@ -4100,6 +4404,9 @@ function buildSettingsHtml(safeDefault, appVersion) {
4100
4404
  <button class="railBtn" id="railCompanion" type="button" title="Companion">
4101
4405
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
4102
4406
  </button>
4407
+ <button class="railBtn" id="railKanban" type="button" title="Kanban">
4408
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
4409
+ </button>
4103
4410
  <button class="railBtn" id="railProjects" type="button" title="Projects">
4104
4411
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
4105
4412
  </button>
@@ -4215,5 +4522,175 @@ function buildSettingsHtml(safeDefault, appVersion) {
4215
4522
  </html>`;
4216
4523
  }
4217
4524
 
4525
+ function buildKanbanHtml(safeDefault, appVersion) {
4526
+ const safeVersion = escapeHtml(appVersion || 'unknown');
4527
+ const kanbanSvg = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>';
4528
+ return `<!doctype html>
4529
+ <html>
4530
+ <head>
4531
+ <meta charset="utf-8" />
4532
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4533
+ <title>FREYA Kanban</title>
4534
+ <link rel="stylesheet" href="/app.css" />
4535
+ </head>
4536
+ <body data-page="kanban">
4537
+ <div class="app">
4538
+ <div class="frame">
4539
+ <div class="shell">
4540
+
4541
+ <aside class="rail" role="navigation" aria-label="Menu principal">
4542
+ <div class="railTop">
4543
+ <div class="railLogo">F</div>
4544
+ </div>
4545
+ <div class="railNav">
4546
+ <button class="railBtn" id="railDashboard" type="button" title="Dashboard">
4547
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg>
4548
+ </button>
4549
+ <button class="railBtn" id="railReports" type="button" title="Relat\\u00f3rios">
4550
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
4551
+ </button>
4552
+ <button class="railBtn" id="railCompanion" type="button" title="Companion">
4553
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
4554
+ </button>
4555
+ <button class="railBtn active" id="railKanban" type="button" title="Kanban">
4556
+ ${kanbanSvg}
4557
+ </button>
4558
+ <button class="railBtn" id="railProjects" type="button" title="Projects">
4559
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
4560
+ </button>
4561
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">
4562
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
4563
+ </button>
4564
+ <button class="railBtn" id="railGraph" type="button" title="Grafo">
4565
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>
4566
+ </button>
4567
+ </div>
4568
+ <div class="railBottom">
4569
+ <button id="railSettings" class="railBtn" title="Configura\\u00e7\\u00f5es" onclick="if (document.body.dataset.page !== 'settings') window.location.href='/settings';">
4570
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
4571
+ </button>
4572
+ </div>
4573
+ </aside>
4574
+
4575
+ <main class="center" role="main">
4576
+ <div class="topbar">
4577
+ <div class="brandLine">
4578
+ <span class="spark"></span>
4579
+ <div class="brandStack">
4580
+ <div class="brand">FREYA</div>
4581
+ <div class="brandSub">Kanban Board &mdash; Vis\\u00e3o SM</div>
4582
+ </div>
4583
+ </div>
4584
+ <div class="topActions">
4585
+ <span class="chip clickable" onclick="window.location.href='/docs'">Docs</span>
4586
+ <span class="chip" id="chipVersion">v${safeVersion}</span>
4587
+ </div>
4588
+ </div>
4589
+
4590
+ <div class="centerBody">
4591
+ <!-- Kanban toolbar -->
4592
+ <div class="kanban-toolbar" style="display:flex; justify-content:space-between; align-items:center; padding:0 0 16px; gap:12px; flex-wrap:wrap;">
4593
+ <div style="display:flex; gap:8px; align-items:center;">
4594
+ <select id="kanbanFilterProject" class="kanban-filter" onchange="window.filterKanban()">
4595
+ <option value="">Todos os projetos</option>
4596
+ </select>
4597
+ <button class="btn small" type="button" onclick="window.loadKanban()">
4598
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px;margin-right:4px"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
4599
+ Atualizar
4600
+ </button>
4601
+ </div>
4602
+ <button class="btn primary small" type="button" onclick="window.openQuickAdd()">+ Nova Task</button>
4603
+ </div>
4604
+
4605
+ <!-- Delta banner -->
4606
+ <div id="kanbanDelta" style="display:none;"></div>
4607
+
4608
+ <!-- Kanban columns -->
4609
+ <div id="kanbanBoard" class="kanban-board">
4610
+ <div class="kanban-col" data-category="DO_NOW">
4611
+ <div class="kanban-col-head do-now">
4612
+ <span class="kanban-col-title">DO NOW</span>
4613
+ <span class="kanban-col-count" id="countDoNow">0</span>
4614
+ </div>
4615
+ <div class="kanban-col-body" id="colDoNow"></div>
4616
+ </div>
4617
+ <div class="kanban-col" data-category="SCHEDULE">
4618
+ <div class="kanban-col-head schedule">
4619
+ <span class="kanban-col-title">SCHEDULE</span>
4620
+ <span class="kanban-col-count" id="countSchedule">0</span>
4621
+ </div>
4622
+ <div class="kanban-col-body" id="colSchedule"></div>
4623
+ </div>
4624
+ <div class="kanban-col" data-category="DELEGATE">
4625
+ <div class="kanban-col-head delegate">
4626
+ <span class="kanban-col-title">DELEGATE</span>
4627
+ <span class="kanban-col-count" id="countDelegate">0</span>
4628
+ </div>
4629
+ <div class="kanban-col-body" id="colDelegate"></div>
4630
+ </div>
4631
+ <div class="kanban-col" data-category="COMPLETED">
4632
+ <div class="kanban-col-head done">
4633
+ <span class="kanban-col-title">DONE (7d)</span>
4634
+ <span class="kanban-col-count" id="countDone">0</span>
4635
+ </div>
4636
+ <div class="kanban-col-body" id="colDone"></div>
4637
+ </div>
4638
+ </div>
4639
+
4640
+ <!-- Blockers strip below kanban -->
4641
+ <div id="kanbanBlockers" style="margin-top:20px; display:none;">
4642
+ <div style="font-size:13px; font-weight:700; color:var(--text); margin-bottom:8px; display:flex; align-items:center; gap:6px;">
4643
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
4644
+ Blockers Ativos
4645
+ </div>
4646
+ <div id="kanbanBlockersList" class="kanban-blockers-list"></div>
4647
+ </div>
4648
+ </div>
4649
+ </main>
4650
+ </div>
4651
+ </div>
4652
+ </div>
4653
+
4654
+ <!-- Quick-add modal -->
4655
+ <div id="quickAddOverlay" class="qa-overlay" style="display:none;">
4656
+ <div class="qa-modal">
4657
+ <div class="qa-header">
4658
+ <span style="font-weight:700; font-size:14px;">Nova Task</span>
4659
+ <button class="btn small" type="button" onclick="window.closeQuickAdd()" style="padding:2px 8px;">&times;</button>
4660
+ </div>
4661
+ <textarea id="qaDesc" class="qa-input" placeholder="Descri\\u00e7\\u00e3o da task..." rows="3"></textarea>
4662
+ <div class="qa-row">
4663
+ <select id="qaCat" class="qa-select">
4664
+ <option value="DO_NOW">DO_NOW</option>
4665
+ <option value="SCHEDULE">SCHEDULE</option>
4666
+ <option value="DELEGATE">DELEGATE</option>
4667
+ </select>
4668
+ <select id="qaPriority" class="qa-select">
4669
+ <option value="">Prioridade</option>
4670
+ <option value="critical">Cr\\u00edtica</option>
4671
+ <option value="high">Alta</option>
4672
+ <option value="medium">M\\u00e9dia</option>
4673
+ <option value="low">Baixa</option>
4674
+ </select>
4675
+ </div>
4676
+ <div class="qa-row">
4677
+ <input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
4678
+ <input id="qaDue" type="date" class="qa-input" style="width:160px;" />
4679
+ </div>
4680
+ <div class="qa-row" style="justify-content:flex-end;">
4681
+ <button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
4682
+ <button class="btn primary small" type="button" onclick="window.submitQuickAdd()">Criar Task</button>
4683
+ </div>
4684
+ </div>
4685
+ </div>
4686
+
4687
+ <script>
4688
+ window.__FREYA_DEFAULT_DIR = "${safeDefault}";
4689
+ </script>
4690
+ <script src="/app.js"></script>
4691
+ </body>
4692
+ </html>`;
4693
+ }
4694
+
4218
4695
  module.exports = { cmdWeb };
4219
4696