@cccarv82/freya 2.14.1 → 2.16.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
@@ -341,6 +341,11 @@
341
341
  persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
342
342
  }
343
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
+
344
349
  // keep newest in view
345
350
  try {
346
351
  thread.scrollTop = thread.scrollHeight;
@@ -477,8 +482,8 @@
477
482
  const thread = $('chatThread');
478
483
  if (!thread) return;
479
484
  const hasContent = thread.children.length > 0;
485
+ thread.style.display = hasContent ? 'flex' : 'none';
480
486
  thread.style.padding = hasContent ? '12px' : '0';
481
- thread.style.maxHeight = hasContent ? '280px' : '0';
482
487
  thread.style.borderTop = hasContent ? '1px solid var(--border)' : 'none';
483
488
  }
484
489
 
@@ -1431,6 +1436,7 @@
1431
1436
  const health = $('railCompanion');
1432
1437
  const graph = $('railGraph');
1433
1438
  const docs = $('railDocs');
1439
+ const kanban = $('railKanban');
1434
1440
 
1435
1441
  const curPage = (document.body && document.body.dataset) ? document.body.dataset.page : null;
1436
1442
  const isDashboard = !curPage || curPage === 'dashboard';
@@ -1460,6 +1466,11 @@
1460
1466
  if (curPage !== 'companion') window.location.href = '/companion';
1461
1467
  };
1462
1468
  }
1469
+ if (kanban) {
1470
+ kanban.onclick = () => {
1471
+ if (curPage !== 'kanban') window.location.href = '/kanban';
1472
+ };
1473
+ }
1463
1474
  if (tl) {
1464
1475
  tl.onclick = () => {
1465
1476
  if (curPage !== 'timeline') window.location.href = '/timeline';
@@ -1534,229 +1545,9 @@
1534
1545
  return '#94a3b8';
1535
1546
  }
1536
1547
 
1537
- function renderSwimlanes(tasks, blockers) {
1538
- const el = $('swimlaneContainer');
1539
- if (!el) return;
1540
- el.innerHTML = '';
1541
-
1542
- // Update top-level summary chips
1543
- const chips = $('focusSummaryChips');
1544
- if (chips) {
1545
- chips.innerHTML = '';
1546
- if (blockers.length > 0) {
1547
- const bc = document.createElement('span');
1548
- bc.style.cssText = 'font-size:11px; font-weight:700; padding:2px 8px; border-radius:10px; background:rgba(239,68,68,0.12); color:#f87171; border:1px solid rgba(239,68,68,0.3);';
1549
- bc.textContent = blockers.length + ' blocker' + (blockers.length > 1 ? 's' : '');
1550
- chips.appendChild(bc);
1551
- }
1552
- if (tasks.length > 0) {
1553
- const tc = document.createElement('span');
1554
- tc.style.cssText = 'font-size:11px; font-weight:700; padding:2px 8px; border-radius:10px; background:rgba(34,197,94,0.10); color:#4ade80; border:1px solid rgba(34,197,94,0.25);';
1555
- tc.textContent = tasks.length + ' task' + (tasks.length > 1 ? 's' : '');
1556
- chips.appendChild(tc);
1557
- }
1558
- }
1559
-
1560
- // Update progress bar (tasks completed today vs total — we show pending count)
1561
- const progWrap = $('focusProgressWrap');
1562
- const progBar = $('focusProgressBar');
1563
- const progLabel = $('focusProgressLabel');
1564
- if (progWrap) {
1565
- if (tasks.length > 0 || blockers.length > 0) {
1566
- progWrap.style.display = 'flex';
1567
- // We only have pending tasks here; show blockers impact
1568
- const blocker_pct = blockers.length === 0 ? 100 : Math.max(10, Math.round((1 - blockers.length / Math.max(tasks.length + blockers.length, 1)) * 100));
1569
- if (progBar) progBar.style.width = blocker_pct + '%';
1570
- if (progBar) progBar.style.background = blockers.length > 0 ? '#f97316' : 'var(--accent)';
1571
- if (progLabel) progLabel.textContent = blockers.length > 0 ? blockers.length + ' blocking' : 'tudo livre';
1572
- } else {
1573
- progWrap.style.display = 'none';
1574
- }
1575
- }
1576
-
1577
- const groups = {};
1578
- const addGroup = (slug) => {
1579
- const key = slug || 'Global / Sem Projeto';
1580
- if (!groups[key]) groups[key] = { tasks: [], blockers: [] };
1581
- return key;
1582
- };
1583
-
1584
- for (const t of tasks) groups[addGroup(t.projectSlug)].tasks.push(t);
1585
- for (const b of blockers) groups[addGroup(b.projectSlug)].blockers.push(b);
1586
-
1587
- const sortedKeys = Object.keys(groups).sort((a, b) => {
1588
- if (a === 'Global / Sem Projeto') return 1;
1589
- if (b === 'Global / Sem Projeto') return -1;
1590
- return a.localeCompare(b);
1591
- });
1592
- sortedKeys.sort((a, b) => groups[b].blockers.length - groups[a].blockers.length);
1593
-
1594
- if (sortedKeys.length === 0) {
1595
- const empty = document.createElement('div');
1596
- empty.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 24px; gap:8px; flex:1;';
1597
- empty.innerHTML = '<div style="font-size:28px; line-height:1;">✅</div>'
1598
- + '<div style="font-size:14px; font-weight:600; color:var(--text);">Dia limpo!</div>'
1599
- + '<div style="font-size:12px; color:var(--muted); text-align:center;">Nenhuma tarefa pendente (DO_NOW) nem bloqueios em aberto. Use o campo acima para registrar o que estiver fazendo.</div>';
1600
- el.appendChild(empty);
1601
- return;
1602
- }
1603
-
1604
- const createTaskRow = (t) => {
1605
- const row = document.createElement('div');
1606
- const pc = priColor(t.priority);
1607
- row.style.cssText = 'display:flex; justify-content:space-between; gap:10px; align-items:center; padding:10px 16px; background:var(--bg); border-bottom:1px solid var(--border); transition:background 0.15s;';
1608
- row.onmouseover = () => row.style.background = 'var(--paper)';
1609
- row.onmouseout = () => row.style.background = 'var(--bg)';
1610
- row.innerHTML =
1611
- '<div style="display:flex; align-items:center; gap:10px; min-width:0; flex:1;">'
1612
- + '<div style="width:8px; height:8px; border-radius:50%; background:' + pc.dot + '; flex-shrink:0;" title="Prioridade: ' + escapeHtml(pc.label) + '"></div>'
1613
- + '<div style="min-width:0;">'
1614
- + '<div style="font-weight:600; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">' + escapeHtml(t.description || '') + '</div>'
1615
- + '<div style="font-size:11px; color:var(--muted); margin-top:2px;">'
1616
- + escapeHtml(pc.label)
1617
- + (t.category ? ' · <span style="font-family:var(--mono);">' + escapeHtml(t.category) + '</span>' : '')
1618
- + '</div>'
1619
- + '</div></div>'
1620
- + '<div class="task-actions" style="display:flex; gap:6px; flex-shrink:0; align-items:center;">'
1621
- + '<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" title="Marcar como concluída">✓ Concluir</button>'
1622
- + '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">✎</button>'
1623
- + '</div>';
1624
- row.querySelector('.edit-btn').onclick = () => editTask(t);
1625
-
1626
- var attachCompleteHandler = function() {
1627
- var cBtn = row.querySelector('.complete-btn');
1628
- if (!cBtn) return;
1629
- cBtn.onclick = function() {
1630
- var actionsDiv = row.querySelector('.task-actions');
1631
- actionsDiv.innerHTML =
1632
- '<input type="text" class="comment-input" placeholder="Comentario (opcional)" '
1633
- + 'style="font-size:11px; padding:3px 8px; background:var(--bg); border:1px solid var(--border); '
1634
- + 'color:var(--text); width:180px; outline:none; font-family:var(--mono);" />'
1635
- + '<button class="btn small confirm-btn" type="button" style="padding:3px 10px; font-size:11px; '
1636
- + 'background:var(--accent); color:#000; font-weight:700;">Confirmar</button>'
1637
- + '<button class="btn small cancel-btn" type="button" style="padding:3px 8px; font-size:11px; '
1638
- + 'color:var(--muted);">\u2715</button>';
1639
-
1640
- var input = actionsDiv.querySelector('.comment-input');
1641
- input.focus();
1642
-
1643
- var doComplete = async function() {
1644
- var comment = input.value.trim();
1645
- actionsDiv.querySelector('.confirm-btn').disabled = true;
1646
- actionsDiv.querySelector('.confirm-btn').textContent = '\u2026';
1647
- try {
1648
- setPill('run', 'concluindo\u2026');
1649
- var body = { dir: dirOrDefault(), id: t.id };
1650
- if (comment) body.comment = comment;
1651
- await api('/api/tasks/complete', body);
1652
- row.style.opacity = '0.4';
1653
- row.style.pointerEvents = 'none';
1654
- await refreshToday();
1655
- setPill('ok', 'conclu\u00edda');
1656
- setTimeout(function() { setPill('ok', 'pronto'); }, 800);
1657
- } catch (e) {
1658
- actionsDiv.querySelector('.confirm-btn').disabled = false;
1659
- actionsDiv.querySelector('.confirm-btn').textContent = 'Confirmar';
1660
- setPill('err', 'falhou');
1661
- }
1662
- };
1663
-
1664
- actionsDiv.querySelector('.confirm-btn').onclick = doComplete;
1665
- input.onkeydown = function(e) { if (e.key === 'Enter') doComplete(); };
1666
- actionsDiv.querySelector('.cancel-btn').onclick = function() {
1667
- actionsDiv.innerHTML =
1668
- '<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" '
1669
- + 'title="Marcar como conclu\u00edda">\u2713 Concluir</button>'
1670
- + '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">\u270E</button>';
1671
- actionsDiv.querySelector('.edit-btn').onclick = function() { editTask(t); };
1672
- attachCompleteHandler();
1673
- };
1674
- };
1675
- };
1676
- attachCompleteHandler();
1677
- return row;
1678
- };
1679
-
1680
- const createBlockerRow = (b) => {
1681
- const row = document.createElement('div');
1682
- const sev = String(b.severity || '').toUpperCase();
1683
- const color = sevColor(sev);
1684
- const when = fmtWhen(new Date(b.createdAt || Date.now()).getTime());
1685
- row.style.cssText = 'display:flex; justify-content:space-between; gap:10px; align-items:center; padding:10px 16px; border-bottom:1px solid rgba(239,68,68,0.15); border-left:3px solid ' + color + '; background:rgba(239,68,68,0.04); transition:background 0.15s;';
1686
- row.onmouseover = () => row.style.background = 'rgba(239,68,68,0.08)';
1687
- row.onmouseout = () => row.style.background = 'rgba(239,68,68,0.04)';
1688
- row.innerHTML =
1689
- '<div style="display:flex; align-items:flex-start; gap:10px; min-width:0; flex:1;">'
1690
- + '<div style="flex-shrink:0; margin-top:1px;">'
1691
- + '<span style="font-size:10px; font-weight:800; letter-spacing:0.5px; padding:2px 6px; border-radius:4px; background:' + color + '22; color:' + color + '; border:1px solid ' + color + '44;">' + escapeHtml(sev || 'BLOCKER') + '</span>'
1692
- + '</div>'
1693
- + '<div style="min-width:0; flex:1;">'
1694
- + '<div style="font-weight:600; font-size:13px; color:var(--text);">' + escapeHtml(b.title || '') + '</div>'
1695
- + '<div style="font-size:11px; color:var(--muted); margin-top:3px;">🕐 ' + escapeHtml(when) + '</div>'
1696
- + '</div></div>'
1697
- + '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px; flex-shrink:0; border-color:' + color + '66; color:' + color + ';">✎</button>';
1698
- row.querySelector('.edit-btn').onclick = () => editBlocker(b);
1699
- return row;
1700
- };
1701
-
1702
- for (const key of sortedKeys) {
1703
- const g = groups[key];
1704
- const bCount = g.blockers.length;
1705
- const tCount = g.tasks.length;
1706
- const hasBlockers = bCount > 0;
1707
-
1708
- const swimlane = document.createElement('div');
1709
- swimlane.style.cssText = 'border-bottom:1px solid var(--border); overflow:hidden;';
1710
-
1711
- const head = document.createElement('div');
1712
- head.style.cssText = 'display:flex; justify-content:space-between; align-items:center; cursor:pointer; padding:9px 16px; background:var(--bg2); transition:background 0.15s;'
1713
- + (hasBlockers ? 'border-left:3px solid #f97316;' : 'border-left:3px solid transparent;');
1714
- head.onmouseover = () => head.style.background = 'var(--paper2)';
1715
- head.onmouseout = () => head.style.background = 'var(--bg2)';
1716
-
1717
- head.innerHTML = `
1718
- <div style="display:flex; align-items:center; gap:8px; min-width:0;">
1719
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; opacity:0.5; transition:transform 0.2s;"><polyline points="6 9 12 15 18 9"></polyline></svg>
1720
- <span style="font-family:var(--mono); font-size:12px; font-weight:700; color:var(--accent); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(key)}</span>
1721
- </div>
1722
- <div style="display:flex; gap:5px; align-items:center; flex-shrink:0;">
1723
- ${bCount > 0 ? `<span style="font-size:11px; font-weight:700; padding:2px 7px; border-radius:10px; background:rgba(239,68,68,0.12); color:#f87171; border:1px solid rgba(239,68,68,0.3);">⛔ ${bCount}</span>` : ''}
1724
- ${tCount > 0 ? `<span style="font-size:11px; font-weight:700; padding:2px 7px; border-radius:10px; background:rgba(34,197,94,0.10); color:#4ade80; border:1px solid rgba(34,197,94,0.25);">✓ ${tCount}</span>` : ''}
1725
- </div>
1726
- `;
1727
-
1728
- const body = document.createElement('div');
1729
- body.style.cssText = 'display:flex; flex-direction:column; background:var(--bg);';
1730
-
1731
- for (const b of g.blockers) body.appendChild(createBlockerRow(b));
1732
- for (const t of g.tasks) body.appendChild(createTaskRow(t));
1733
-
1734
- let isOpen = true;
1735
- head.onclick = () => {
1736
- isOpen = !isOpen;
1737
- body.style.display = isOpen ? 'flex' : 'none';
1738
- const svg = head.querySelector('svg');
1739
- if (svg) svg.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
1740
- };
1741
-
1742
- swimlane.appendChild(head);
1743
- swimlane.appendChild(body);
1744
- el.appendChild(swimlane);
1745
- }
1746
- }
1747
-
1548
+ // refreshToday now delegates to loadKanban (dashboard uses embedded kanban board)
1748
1549
  async function refreshToday() {
1749
- showSkeleton('swimlaneContainer');
1750
- try {
1751
- const [t, b] = await Promise.all([
1752
- api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 50 }),
1753
- api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 50 })
1754
- ]);
1755
- renderSwimlanes((t && t.tasks) || [], (b && b.blockers) || []);
1756
- refreshBlockersInsights();
1757
- } catch (e) {
1758
- // keep silent in background refresh
1759
- }
1550
+ await loadKanban();
1760
1551
  }
1761
1552
 
1762
1553
  function renderBlockersInsights(payload) {
@@ -2769,6 +2560,7 @@
2769
2560
  const isTimelinePage = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
2770
2561
  const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
2771
2562
  const isGraphPage = document.body && document.body.dataset && document.body.dataset.page === 'graph';
2563
+ const isKanbanPage = document.body && document.body.dataset && document.body.dataset.page === 'kanban';
2772
2564
 
2773
2565
  // Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
2774
2566
  (async () => {
@@ -2819,6 +2611,11 @@
2819
2611
  return;
2820
2612
  }
2821
2613
 
2614
+ if (isKanbanPage) {
2615
+ await loadKanban();
2616
+ return;
2617
+ }
2618
+
2822
2619
  // If workspace isn't initialized yet, auto-init (reduces clicks)
2823
2620
  try {
2824
2621
  if (defaults && defaults.workspaceOk === false) {
@@ -2847,16 +2644,20 @@
2847
2644
  if (typeof saveAndPlan === 'function') saveAndPlan();
2848
2645
  return;
2849
2646
  }
2850
- // Ctrl/Cmd+K: Focus search/input on current page
2647
+ // Ctrl/Cmd+K: Open quick-add modal
2851
2648
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
2852
2649
  e.preventDefault();
2853
- const target = $('inboxText') || $('reportsFilter') || $('projectsFilter') || $('timelineFilter');
2854
- if (target) target.focus();
2650
+ openQuickAdd();
2855
2651
  return;
2856
2652
  }
2857
- // Escape: Blur active element
2858
- if (e.key === 'Escape' && document.activeElement) {
2859
- document.activeElement.blur();
2653
+ // Escape: Close quick-add modal or blur active element
2654
+ if (e.key === 'Escape') {
2655
+ const overlay = $('quickAddOverlay');
2656
+ if (overlay && overlay.style.display !== 'none') {
2657
+ closeQuickAdd();
2658
+ return;
2659
+ }
2660
+ if (document.activeElement) document.activeElement.blur();
2860
2661
  }
2861
2662
  });
2862
2663
 
@@ -2912,4 +2713,468 @@
2912
2713
  window.askFreya = askFreya;
2913
2714
  window.askFreyaInline = askFreyaInline;
2914
2715
  window.askFreyaFromInput = askFreyaFromInput;
2716
+
2717
+ /* ── Quick-Add Modal ── */
2718
+
2719
+ // Wire hidden date picker → text input (once)
2720
+ var _qaDateWired = false;
2721
+ function wireQaDatePicker() {
2722
+ if (_qaDateWired) return;
2723
+ var picker = $('qaDuePicker');
2724
+ var text = $('qaDue');
2725
+ if (!picker || !text) return;
2726
+ _qaDateWired = true;
2727
+ picker.addEventListener('change', function() {
2728
+ if (picker.value) {
2729
+ text.value = fmtDateBR(picker.value);
2730
+ }
2731
+ });
2732
+ // Auto-format on blur: accept dd/mm/aaaa typed manually
2733
+ text.addEventListener('keyup', function(e) {
2734
+ var v = text.value.replace(/[^\d]/g, '');
2735
+ if (v.length >= 3 && text.value.indexOf('/') < 0) {
2736
+ // Auto-insert slashes: dd/mm/aaaa
2737
+ var formatted = v.slice(0, 2);
2738
+ if (v.length >= 3) formatted += '/' + v.slice(2, 4);
2739
+ if (v.length >= 5) formatted += '/' + v.slice(4, 8);
2740
+ text.value = formatted;
2741
+ }
2742
+ });
2743
+ }
2744
+
2745
+ function openQuickAdd() {
2746
+ const overlay = $('quickAddOverlay');
2747
+ if (!overlay) return;
2748
+ overlay.style.display = 'flex';
2749
+ wireQaDatePicker();
2750
+ const desc = $('qaDesc');
2751
+ if (desc) { desc.value = ''; desc.focus(); }
2752
+ var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
2753
+ var pri = $('qaPriority'); if (pri) pri.value = '';
2754
+ var slug = $('qaSlug'); if (slug) slug.value = '';
2755
+ var due = $('qaDue'); if (due) due.value = '';
2756
+ var picker = $('qaDuePicker'); if (picker) picker.value = '';
2757
+ }
2758
+
2759
+ function closeQuickAdd() {
2760
+ var overlay = $('quickAddOverlay');
2761
+ if (overlay) overlay.style.display = 'none';
2762
+ }
2763
+
2764
+ // Parse dd/mm/aaaa → YYYY-MM-DD
2765
+ function parseDateBR(str) {
2766
+ if (!str) return '';
2767
+ var s = str.trim();
2768
+ if (s.indexOf('/') > -1) {
2769
+ var p = s.split('/');
2770
+ if (p.length === 3 && p[0].length === 2 && p[1].length === 2 && p[2].length === 4) {
2771
+ return p[2] + '-' + p[1] + '-' + p[0];
2772
+ }
2773
+ }
2774
+ return s; // fallback: pass as-is (may be YYYY-MM-DD already)
2775
+ }
2776
+
2777
+ async function submitQuickAdd() {
2778
+ var desc = $('qaDesc');
2779
+ var text = desc ? desc.value.trim() : '';
2780
+ if (!text) { showToast('err', 'Descricao obrigatoria'); return; }
2781
+
2782
+ var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
2783
+ var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
2784
+ var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
2785
+ var due = $('qaDue'); var dueVal = due ? parseDateBR(due.value) : '';
2786
+
2787
+ var body = { dir: dirOrDefault(), description: text, category: catVal };
2788
+ if (priVal) body.priority = priVal;
2789
+ if (slugVal) body.projectSlug = slugVal;
2790
+ if (dueVal) body.dueDate = dueVal;
2791
+
2792
+ try {
2793
+ await api('/api/tasks/create', body);
2794
+ closeQuickAdd();
2795
+ showToast('ok', 'Task criada');
2796
+ await loadKanban();
2797
+ } catch (e) {
2798
+ showToast('err', 'Erro ao criar task');
2799
+ }
2800
+ }
2801
+
2802
+ window.openQuickAdd = openQuickAdd;
2803
+ window.closeQuickAdd = closeQuickAdd;
2804
+ window.submitQuickAdd = submitQuickAdd;
2805
+
2806
+ /* ── Delta Banner ── */
2807
+ async function loadDelta() {
2808
+ var el = $('kanbanDelta') || $('deltaBanner');
2809
+ if (!el) return;
2810
+ try {
2811
+ var res = await api('/api/summary/delta', { dir: dirOrDefault() });
2812
+ if (!res || !res.delta) { el.style.display = 'none'; return; }
2813
+ var d = res.delta;
2814
+ var parts = [];
2815
+ if (d.completedTasks > 0) parts.push(d.completedTasks + ' concluida(s)');
2816
+ if (d.resolvedBlockers > 0) parts.push(d.resolvedBlockers + ' blocker(s) resolvido(s)');
2817
+ if (d.newTasks > 0) parts.push(d.newTasks + ' nova(s)');
2818
+ if (d.newBlockers > 0) parts.push(d.newBlockers + ' novo(s) blocker(s)');
2819
+ if (d.overdueTasks > 0) parts.push(d.overdueTasks + ' atrasada(s)');
2820
+ if (parts.length === 0) { el.style.display = 'none'; return; }
2821
+ el.style.display = 'flex';
2822
+ el.className = 'delta-banner';
2823
+ 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>'
2824
+ + '<span>Ultimas 24h: ' + escapeHtml(parts.join(' \u00b7 ')) + '</span>';
2825
+ } catch { el.style.display = 'none'; }
2826
+ }
2827
+
2828
+ /* ── Kanban Board ── */
2829
+ var _kanbanData = { tasks: [], blockers: [] };
2830
+
2831
+ async function loadKanban() {
2832
+ try {
2833
+ var res = await api('/api/tasks/kanban', { dir: dirOrDefault() });
2834
+ if (!res || !res.ok) return;
2835
+ _kanbanData.tasks = res.tasks || [];
2836
+ _kanbanData.blockers = res.blockers || [];
2837
+ populateKanbanProjects();
2838
+ renderKanban();
2839
+ renderKanbanBlockers();
2840
+ loadDelta();
2841
+ // On dashboard, also refresh blockers insights panel
2842
+ if ($('blockersInsightsWrap')) refreshBlockersInsights();
2843
+ } catch (e) {
2844
+ showToast('err', 'Erro ao carregar kanban');
2845
+ }
2846
+ }
2847
+
2848
+ function populateKanbanProjects() {
2849
+ var sel = $('kanbanFilterProject');
2850
+ if (!sel) return;
2851
+ var slugs = new Set();
2852
+ _kanbanData.tasks.forEach(function(t) { if (t.projectSlug) slugs.add(t.projectSlug); });
2853
+ _kanbanData.blockers.forEach(function(b) { if (b.projectSlug) slugs.add(b.projectSlug); });
2854
+ var current = sel.value;
2855
+ sel.innerHTML = '<option value="">Todos os projetos</option>';
2856
+ Array.from(slugs).sort().forEach(function(s) {
2857
+ var opt = document.createElement('option');
2858
+ opt.value = s; opt.textContent = s;
2859
+ sel.appendChild(opt);
2860
+ });
2861
+ sel.value = current;
2862
+ }
2863
+
2864
+ function filterKanban() { renderKanban(); renderKanbanBlockers(); }
2865
+
2866
+ function getFilteredTasks() {
2867
+ var sel = $('kanbanFilterProject');
2868
+ var filter = sel ? sel.value : '';
2869
+ if (!filter) return _kanbanData.tasks;
2870
+ return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
2871
+ }
2872
+
2873
+ // Format ISO date (YYYY-MM-DD or full ISO) to dd/mm/aaaa
2874
+ function fmtDateBR(isoStr) {
2875
+ if (!isoStr) return '';
2876
+ var d = isoStr.slice(0, 10); // YYYY-MM-DD
2877
+ var parts = d.split('-');
2878
+ if (parts.length !== 3) return d;
2879
+ return parts[2] + '/' + parts[1] + '/' + parts[0];
2880
+ }
2881
+
2882
+ // Format ISO datetime to dd/mm/aaaa HH:mm
2883
+ function fmtDateTimeBR(isoStr) {
2884
+ if (!isoStr) return '';
2885
+ var dt = new Date(isoStr);
2886
+ if (isNaN(dt.getTime())) return isoStr;
2887
+ var dd = String(dt.getDate()).padStart(2, '0');
2888
+ var mm = String(dt.getMonth() + 1).padStart(2, '0');
2889
+ var yyyy = dt.getFullYear();
2890
+ var hh = String(dt.getHours()).padStart(2, '0');
2891
+ var min = String(dt.getMinutes()).padStart(2, '0');
2892
+ return dd + '/' + mm + '/' + yyyy + ' ' + hh + ':' + min;
2893
+ }
2894
+
2895
+ // Show detail panel for a task or blocker
2896
+ function showDetailPanel(item, type) {
2897
+ // Remove existing panel
2898
+ var old = document.querySelector('.detail-panel-overlay');
2899
+ if (old) old.remove();
2900
+
2901
+ var overlay = document.createElement('div');
2902
+ overlay.className = 'detail-panel-overlay';
2903
+ overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
2904
+
2905
+ var panel = document.createElement('div');
2906
+ panel.className = 'detail-panel';
2907
+
2908
+ var isTask = type === 'task';
2909
+ var pc = isTask ? priColor(item.priority) : null;
2910
+
2911
+ var html = '<div class="detail-panel-header">';
2912
+ html += '<div style="display:flex; align-items:center; gap:10px; flex:1; min-width:0;">';
2913
+ if (isTask && pc) {
2914
+ html += '<span class="kanban-pri-dot" style="background:' + pc.dot + '; width:12px; height:12px;"></span>';
2915
+ } else {
2916
+ var sColor = sevColor(item.severity);
2917
+ html += '<span style="font-size:11px; font-weight:800; padding:3px 8px; border-radius:4px; background:' + sColor + '22; color:' + sColor + '; border:1px solid ' + sColor + '44;">' + escapeHtml(item.severity || '') + '</span>';
2918
+ }
2919
+ html += '<span class="detail-panel-title">' + escapeHtml(isTask ? item.description : item.title) + '</span>';
2920
+ html += '</div>';
2921
+ html += '<button class="detail-panel-close" onclick="this.closest(\'.detail-panel-overlay\').remove()">&times;</button>';
2922
+ html += '</div>';
2923
+
2924
+ // Info grid
2925
+ html += '<div class="detail-panel-grid">';
2926
+ if (isTask) {
2927
+ html += detailRow('ID', item.id);
2928
+ html += detailRow('Status', item.status);
2929
+ html += detailRow('Categoria', item.category);
2930
+ html += detailRow('Prioridade', pc ? pc.label : 'Nenhuma');
2931
+ html += detailRow('Projeto', item.projectSlug || '—');
2932
+ html += detailRow('Stream', item.streamSlug || '—');
2933
+ html += detailRow('Due Date', item.dueDate ? fmtDateBR(item.dueDate) : '—');
2934
+ html += detailRow('Criada em', fmtDateTimeBR(item.createdAt));
2935
+ if (item.completedAt) html += detailRow('Concluida em', fmtDateTimeBR(item.completedAt));
2936
+ if (item.source) html += detailRow('Fonte', item.source);
2937
+ } else {
2938
+ html += detailRow('ID', item.id);
2939
+ html += detailRow('Severidade', item.severity);
2940
+ html += detailRow('Status', item.status);
2941
+ html += detailRow('Projeto', item.projectSlug || '—');
2942
+ html += detailRow('Responsavel', item.owner || '—');
2943
+ html += detailRow('Proxima Acao', item.nextAction || '—');
2944
+ html += detailRow('Criado em', fmtDateTimeBR(item.createdAt));
2945
+ if (item.resolvedAt) html += detailRow('Resolvido em', fmtDateTimeBR(item.resolvedAt));
2946
+ if (item.source) html += detailRow('Fonte', item.source);
2947
+ }
2948
+ html += '</div>';
2949
+
2950
+ // Comments section (tasks only)
2951
+ if (isTask && item.comments && item.comments.length > 0) {
2952
+ html += '<div class="detail-panel-section">';
2953
+ html += '<div class="detail-panel-section-title">Comentarios</div>';
2954
+ item.comments.forEach(function(c) {
2955
+ var cObj = typeof c === 'string' ? { text: c } : c;
2956
+ html += '<div class="detail-panel-comment">';
2957
+ if (cObj.at) html += '<span class="detail-panel-comment-date">' + fmtDateTimeBR(cObj.at) + '</span>';
2958
+ html += '<span>' + escapeHtml(cObj.text || cObj.comment || String(c)) + '</span>';
2959
+ html += '</div>';
2960
+ });
2961
+ html += '</div>';
2962
+ }
2963
+
2964
+ // Metadata section (raw, if has extra fields)
2965
+ var metaKeys = item.metadata ? Object.keys(item.metadata).filter(function(k) {
2966
+ return ['priority', 'streamSlug', 'comments', 'source'].indexOf(k) < 0 && item.metadata[k];
2967
+ }) : [];
2968
+ if (metaKeys.length > 0) {
2969
+ html += '<div class="detail-panel-section">';
2970
+ html += '<div class="detail-panel-section-title">Metadados Adicionais</div>';
2971
+ html += '<div class="detail-panel-grid">';
2972
+ metaKeys.forEach(function(k) {
2973
+ html += detailRow(k, typeof item.metadata[k] === 'object' ? JSON.stringify(item.metadata[k]) : String(item.metadata[k]));
2974
+ });
2975
+ html += '</div></div>';
2976
+ }
2977
+
2978
+ panel.innerHTML = html;
2979
+ overlay.appendChild(panel);
2980
+ document.body.appendChild(overlay);
2981
+
2982
+ // Close on Escape
2983
+ var escHandler = function(e) {
2984
+ if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); }
2985
+ };
2986
+ document.addEventListener('keydown', escHandler);
2987
+ }
2988
+
2989
+ function detailRow(label, value) {
2990
+ return '<div class="detail-row-label">' + escapeHtml(label) + '</div>'
2991
+ + '<div class="detail-row-value">' + escapeHtml(String(value || '')) + '</div>';
2992
+ }
2993
+
2994
+ function renderKanban() {
2995
+ var tasks = getFilteredTasks();
2996
+ var today = new Date().toISOString().slice(0, 10);
2997
+ var sevenAgo = new Date(Date.now() - 7 * 86400000).toISOString();
2998
+
2999
+ var cols = {
3000
+ DO_NOW: [], SCHEDULE: [], DELEGATE: [], COMPLETED: []
3001
+ };
3002
+
3003
+ tasks.forEach(function(t) {
3004
+ if (t.status === 'COMPLETED') {
3005
+ if (t.completedAt && t.completedAt >= sevenAgo) cols.COMPLETED.push(t);
3006
+ } else if (t.status === 'PENDING' && cols[t.category]) {
3007
+ cols[t.category].push(t);
3008
+ }
3009
+ });
3010
+
3011
+ var idMap = { DO_NOW: 'colDoNow', SCHEDULE: 'colSchedule', DELEGATE: 'colDelegate', COMPLETED: 'colDone' };
3012
+ var countMap = { DO_NOW: 'countDoNow', SCHEDULE: 'countSchedule', DELEGATE: 'countDelegate', COMPLETED: 'countDone' };
3013
+
3014
+ Object.keys(cols).forEach(function(cat) {
3015
+ var el = $(idMap[cat]);
3016
+ var countEl = $(countMap[cat]);
3017
+ if (countEl) countEl.textContent = cols[cat].length;
3018
+ if (!el) return;
3019
+ el.innerHTML = '';
3020
+
3021
+ cols[cat].forEach(function(t) {
3022
+ var card = document.createElement('div');
3023
+ card.className = 'kanban-card';
3024
+ card.draggable = (cat !== 'COMPLETED');
3025
+ card.dataset.taskId = t.id;
3026
+ card.dataset.category = cat;
3027
+
3028
+ var pc = priColor(t.priority);
3029
+ var isOverdue = t.dueDate && t.dueDate < today && t.status === 'PENDING';
3030
+
3031
+ var html = '<div class="kanban-card-header">'
3032
+ + '<span class="kanban-pri-dot" style="background:' + pc.dot + ';" title="' + escapeHtml(pc.label) + '"></span>'
3033
+ + '<span class="kanban-card-desc">' + escapeHtml(t.description || '') + '</span>'
3034
+ + '</div>';
3035
+
3036
+ var meta = [];
3037
+ if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
3038
+ if (t.dueDate) {
3039
+ var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
3040
+ meta.push('<span class="' + dueCls + '">' + escapeHtml(fmtDateBR(t.dueDate)) + '</span>');
3041
+ }
3042
+ if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
3043
+
3044
+ if (cat !== 'COMPLETED') {
3045
+ html += '<div class="kanban-card-actions">'
3046
+ + '<button class="kanban-action-btn complete-btn" title="Concluir">\u2713</button>'
3047
+ + '<button class="kanban-action-btn edit-btn" title="Editar">\u270E</button>'
3048
+ + '</div>';
3049
+ }
3050
+
3051
+ card.innerHTML = html;
3052
+
3053
+ // Click to show detail panel (only if not dragging)
3054
+ var _dragged = false;
3055
+ card.addEventListener('mousedown', function() { _dragged = false; });
3056
+ card.addEventListener('mousemove', function() { _dragged = true; });
3057
+ card.addEventListener('click', function(e) {
3058
+ if (_dragged) return;
3059
+ // Don't trigger on action buttons
3060
+ if (e.target.closest('.kanban-card-actions')) return;
3061
+ showDetailPanel(t, 'task');
3062
+ });
3063
+
3064
+ // Drag events
3065
+ if (cat !== 'COMPLETED') {
3066
+ card.addEventListener('dragstart', function(e) {
3067
+ e.dataTransfer.setData('text/plain', t.id);
3068
+ e.dataTransfer.effectAllowed = 'move';
3069
+ card.classList.add('dragging');
3070
+ });
3071
+ card.addEventListener('dragend', function() {
3072
+ card.classList.remove('dragging');
3073
+ });
3074
+
3075
+ // Complete button
3076
+ var completeBtn = card.querySelector('.complete-btn');
3077
+ if (completeBtn) {
3078
+ completeBtn.onclick = async function() {
3079
+ try {
3080
+ await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
3081
+ showToast('ok', 'Concluida');
3082
+ await loadKanban();
3083
+ } catch { showToast('err', 'Falhou'); }
3084
+ };
3085
+ }
3086
+
3087
+ // Edit button - inline edit via prompt
3088
+ var editBtn = card.querySelector('.edit-btn');
3089
+ if (editBtn) {
3090
+ editBtn.onclick = async function() {
3091
+ var newCat = prompt('Categoria (DO_NOW|SCHEDULE|DELEGATE):', cat);
3092
+ if (!newCat) return;
3093
+ var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
3094
+ if (newSlug === null) return;
3095
+ var currentDueBR = t.dueDate ? fmtDateBR(t.dueDate) : '';
3096
+ var newDueBR = prompt('Due date (dd/mm/aaaa):', currentDueBR);
3097
+ if (newDueBR === null) return;
3098
+ // Convert dd/mm/aaaa back to YYYY-MM-DD for API
3099
+ var newDue = '';
3100
+ if (newDueBR && newDueBR.indexOf('/') > -1) {
3101
+ var dp = newDueBR.split('/');
3102
+ if (dp.length === 3) newDue = dp[2] + '-' + dp[1] + '-' + dp[0];
3103
+ } else {
3104
+ newDue = newDueBR; // fallback: accept YYYY-MM-DD too
3105
+ }
3106
+ try {
3107
+ await api('/api/tasks/update', {
3108
+ dir: dirOrDefault(), id: t.id,
3109
+ patch: { category: newCat, projectSlug: newSlug, dueDate: newDue || null }
3110
+ });
3111
+ showToast('ok', 'Atualizada');
3112
+ await loadKanban();
3113
+ } catch { showToast('err', 'Falhou'); }
3114
+ };
3115
+ }
3116
+ }
3117
+
3118
+ el.appendChild(card);
3119
+ });
3120
+
3121
+ // Drop zone
3122
+ if (cat !== 'COMPLETED') {
3123
+ el.addEventListener('dragover', function(e) {
3124
+ e.preventDefault();
3125
+ e.dataTransfer.dropEffect = 'move';
3126
+ el.classList.add('drag-over');
3127
+ });
3128
+ el.addEventListener('dragleave', function() {
3129
+ el.classList.remove('drag-over');
3130
+ });
3131
+ el.addEventListener('drop', async function(e) {
3132
+ e.preventDefault();
3133
+ el.classList.remove('drag-over');
3134
+ var taskId = e.dataTransfer.getData('text/plain');
3135
+ if (!taskId) return;
3136
+ try {
3137
+ await api('/api/tasks/update', {
3138
+ dir: dirOrDefault(), id: taskId,
3139
+ patch: { category: cat }
3140
+ });
3141
+ showToast('ok', 'Movida para ' + cat);
3142
+ await loadKanban();
3143
+ } catch { showToast('err', 'Falhou'); }
3144
+ });
3145
+ }
3146
+ });
3147
+ }
3148
+
3149
+ function renderKanbanBlockers() {
3150
+ var wrap = $('kanbanBlockers');
3151
+ var list = $('kanbanBlockersList');
3152
+ if (!wrap || !list) return;
3153
+
3154
+ var sel = $('kanbanFilterProject');
3155
+ var filter = sel ? sel.value : '';
3156
+ var blockers = _kanbanData.blockers;
3157
+ if (filter) blockers = blockers.filter(function(b) { return b.projectSlug === filter; });
3158
+
3159
+ if (blockers.length === 0) { wrap.style.display = 'none'; return; }
3160
+ wrap.style.display = 'block';
3161
+ list.innerHTML = '';
3162
+
3163
+ blockers.forEach(function(b) {
3164
+ var card = document.createElement('div');
3165
+ card.className = 'kanban-blocker-card';
3166
+ var color = sevColor(b.severity);
3167
+ card.innerHTML = '<span class="kanban-blocker-sev" style="background:' + color + '22; color:' + color + '; border-color:' + color + '44;">' + escapeHtml(b.severity || 'BLOCKER') + '</span>'
3168
+ + '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
3169
+ + (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
3170
+ + (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
3171
+ card.style.cursor = 'pointer';
3172
+ card.addEventListener('click', function() { showDetailPanel(b, 'blocker'); });
3173
+ list.appendChild(card);
3174
+ });
3175
+ }
3176
+
3177
+ window.loadKanban = loadKanban;
3178
+ window.filterKanban = filterKanban;
3179
+ window.loadDelta = loadDelta;
2915
3180
  })();