@cccarv82/freya 2.14.1 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/web-ui.css CHANGED
@@ -603,12 +603,15 @@ body {
603
603
 
604
604
  .promptShell {
605
605
  display: flex;
606
- justify-content: center;
606
+ flex-direction: column;
607
+ align-items: stretch;
607
608
  width: 100%;
609
+ /* Occupy ~90% of viewport height minus topbar */
610
+ min-height: calc(90vh - 60px);
608
611
  }
609
612
 
610
613
  .promptBar {
611
- width: min(960px, 100%);
614
+ width: 100%;
612
615
  background: var(--paper);
613
616
  backdrop-filter: blur(16px);
614
617
  -webkit-backdrop-filter: blur(16px);
@@ -1612,4 +1615,302 @@ textarea:focus {
1612
1615
 
1613
1616
  * {
1614
1617
  border-radius: 0 !important;
1618
+ }
1619
+
1620
+ /* ── Kanban Board ── */
1621
+ .kanban-board {
1622
+ display: grid;
1623
+ grid-template-columns: repeat(4, 1fr);
1624
+ gap: 12px;
1625
+ min-height: 400px;
1626
+ }
1627
+
1628
+ @media (max-width: 1100px) {
1629
+ .kanban-board { grid-template-columns: repeat(2, 1fr); }
1630
+ }
1631
+ @media (max-width: 600px) {
1632
+ .kanban-board { grid-template-columns: 1fr; }
1633
+ }
1634
+
1635
+ .kanban-col {
1636
+ display: flex;
1637
+ flex-direction: column;
1638
+ background: var(--bg);
1639
+ border: 1px solid var(--line2);
1640
+ min-height: 300px;
1641
+ }
1642
+
1643
+ .kanban-col-head {
1644
+ display: flex;
1645
+ justify-content: space-between;
1646
+ align-items: center;
1647
+ padding: 10px 14px;
1648
+ font-size: 11px;
1649
+ font-weight: 800;
1650
+ letter-spacing: 1px;
1651
+ text-transform: uppercase;
1652
+ border-bottom: 2px solid var(--line2);
1653
+ flex-shrink: 0;
1654
+ }
1655
+
1656
+ .kanban-col-head.do-now { border-bottom-color: #ef4444; color: #ef4444; }
1657
+ .kanban-col-head.schedule { border-bottom-color: #3b82f6; color: #3b82f6; }
1658
+ .kanban-col-head.delegate { border-bottom-color: #f97316; color: #f97316; }
1659
+ .kanban-col-head.done { border-bottom-color: #22c55e; color: #22c55e; }
1660
+
1661
+ .kanban-col-count {
1662
+ font-size: 12px;
1663
+ font-weight: 700;
1664
+ padding: 1px 8px;
1665
+ background: var(--paper2);
1666
+ border: 1px solid var(--line2);
1667
+ }
1668
+
1669
+ .kanban-col-body {
1670
+ flex: 1;
1671
+ padding: 8px;
1672
+ display: flex;
1673
+ flex-direction: column;
1674
+ gap: 8px;
1675
+ overflow-y: auto;
1676
+ max-height: 600px;
1677
+ min-height: 80px;
1678
+ transition: background 0.15s;
1679
+ }
1680
+
1681
+ .kanban-col-body.drag-over {
1682
+ background: var(--chip);
1683
+ }
1684
+
1685
+ .kanban-card {
1686
+ background: var(--paper);
1687
+ border: 1px solid var(--line2);
1688
+ padding: 10px 12px;
1689
+ cursor: grab;
1690
+ transition: box-shadow 0.15s, opacity 0.15s;
1691
+ display: flex;
1692
+ flex-direction: column;
1693
+ gap: 6px;
1694
+ }
1695
+
1696
+ .kanban-card:hover {
1697
+ box-shadow: var(--shadow2);
1698
+ }
1699
+
1700
+ .kanban-card.dragging {
1701
+ opacity: 0.4;
1702
+ }
1703
+
1704
+ .kanban-card-header {
1705
+ display: flex;
1706
+ align-items: flex-start;
1707
+ gap: 8px;
1708
+ }
1709
+
1710
+ .kanban-pri-dot {
1711
+ width: 8px;
1712
+ height: 8px;
1713
+ flex-shrink: 0;
1714
+ margin-top: 4px;
1715
+ }
1716
+
1717
+ .kanban-card-desc {
1718
+ font-size: 13px;
1719
+ font-weight: 600;
1720
+ color: var(--text);
1721
+ line-height: 1.3;
1722
+ word-break: break-word;
1723
+ }
1724
+
1725
+ .kanban-card-meta {
1726
+ display: flex;
1727
+ gap: 6px;
1728
+ flex-wrap: wrap;
1729
+ align-items: center;
1730
+ }
1731
+
1732
+ .kanban-tag {
1733
+ font-size: 10px;
1734
+ font-weight: 600;
1735
+ padding: 1px 6px;
1736
+ background: var(--chip);
1737
+ color: var(--primary);
1738
+ border: 1px solid var(--line2);
1739
+ font-family: var(--mono);
1740
+ }
1741
+
1742
+ .kanban-due {
1743
+ font-size: 10px;
1744
+ font-weight: 600;
1745
+ padding: 1px 6px;
1746
+ color: var(--muted);
1747
+ border: 1px solid var(--line2);
1748
+ }
1749
+
1750
+ .kanban-due.overdue {
1751
+ background: rgba(239, 68, 68, 0.1);
1752
+ color: #ef4444;
1753
+ border-color: rgba(239, 68, 68, 0.3);
1754
+ }
1755
+
1756
+ .kanban-card-actions {
1757
+ display: flex;
1758
+ gap: 4px;
1759
+ justify-content: flex-end;
1760
+ opacity: 0;
1761
+ transition: opacity 0.15s;
1762
+ }
1763
+
1764
+ .kanban-card:hover .kanban-card-actions {
1765
+ opacity: 1;
1766
+ }
1767
+
1768
+ .kanban-action-btn {
1769
+ background: var(--bg);
1770
+ border: 1px solid var(--line2);
1771
+ color: var(--muted);
1772
+ padding: 2px 8px;
1773
+ font-size: 12px;
1774
+ cursor: pointer;
1775
+ transition: var(--transition);
1776
+ }
1777
+
1778
+ .kanban-action-btn:hover {
1779
+ color: var(--text);
1780
+ background: var(--paper2);
1781
+ }
1782
+
1783
+ .kanban-action-btn.complete-btn:hover {
1784
+ color: #22c55e;
1785
+ border-color: #22c55e;
1786
+ }
1787
+
1788
+ .kanban-owner {
1789
+ font-size: 10px;
1790
+ color: var(--faint);
1791
+ font-style: italic;
1792
+ }
1793
+
1794
+ /* Kanban filter */
1795
+ .kanban-filter {
1796
+ background: var(--bg);
1797
+ border: 1px solid var(--line2);
1798
+ color: var(--text);
1799
+ padding: 5px 10px;
1800
+ font-size: 12px;
1801
+ font-family: var(--mono);
1802
+ min-width: 180px;
1803
+ }
1804
+
1805
+ /* Kanban blockers strip */
1806
+ .kanban-blockers-list {
1807
+ display: flex;
1808
+ flex-wrap: wrap;
1809
+ gap: 8px;
1810
+ }
1811
+
1812
+ .kanban-blocker-card {
1813
+ display: flex;
1814
+ align-items: center;
1815
+ gap: 8px;
1816
+ padding: 8px 12px;
1817
+ background: rgba(239, 68, 68, 0.04);
1818
+ border: 1px solid rgba(239, 68, 68, 0.15);
1819
+ border-left: 3px solid #ef4444;
1820
+ flex: 1 1 300px;
1821
+ max-width: 500px;
1822
+ }
1823
+
1824
+ .kanban-blocker-sev {
1825
+ font-size: 9px;
1826
+ font-weight: 800;
1827
+ letter-spacing: 0.5px;
1828
+ padding: 2px 6px;
1829
+ border: 1px solid;
1830
+ flex-shrink: 0;
1831
+ }
1832
+
1833
+ .kanban-blocker-title {
1834
+ font-size: 12px;
1835
+ font-weight: 600;
1836
+ color: var(--text);
1837
+ flex: 1;
1838
+ min-width: 0;
1839
+ overflow: hidden;
1840
+ text-overflow: ellipsis;
1841
+ white-space: nowrap;
1842
+ }
1843
+
1844
+ /* ── Quick-Add Modal ── */
1845
+ .qa-overlay {
1846
+ position: fixed;
1847
+ inset: 0;
1848
+ background: rgba(0, 0, 0, 0.5);
1849
+ z-index: 9999;
1850
+ display: flex;
1851
+ align-items: center;
1852
+ justify-content: center;
1853
+ }
1854
+
1855
+ .qa-modal {
1856
+ background: var(--bg);
1857
+ border: 1px solid var(--line2);
1858
+ box-shadow: var(--shadow);
1859
+ width: 440px;
1860
+ max-width: 95vw;
1861
+ padding: 16px;
1862
+ display: flex;
1863
+ flex-direction: column;
1864
+ gap: 10px;
1865
+ }
1866
+
1867
+ .qa-header {
1868
+ display: flex;
1869
+ justify-content: space-between;
1870
+ align-items: center;
1871
+ }
1872
+
1873
+ .qa-input {
1874
+ background: var(--paper);
1875
+ border: 1px solid var(--line2);
1876
+ color: var(--text);
1877
+ padding: 8px 10px;
1878
+ font-size: 13px;
1879
+ font-family: var(--sans);
1880
+ width: 100%;
1881
+ resize: none;
1882
+ }
1883
+
1884
+ .qa-input:focus {
1885
+ outline: none;
1886
+ border-color: var(--primary);
1887
+ }
1888
+
1889
+ .qa-select {
1890
+ background: var(--paper);
1891
+ border: 1px solid var(--line2);
1892
+ color: var(--text);
1893
+ padding: 6px 10px;
1894
+ font-size: 12px;
1895
+ flex: 1;
1896
+ }
1897
+
1898
+ .qa-row {
1899
+ display: flex;
1900
+ gap: 8px;
1901
+ align-items: center;
1902
+ }
1903
+
1904
+ /* ── Delta Banner ── */
1905
+ .delta-banner {
1906
+ display: flex;
1907
+ align-items: center;
1908
+ gap: 8px;
1909
+ padding: 8px 14px;
1910
+ margin-bottom: 12px;
1911
+ background: var(--chip);
1912
+ border: 1px solid var(--line2);
1913
+ font-size: 12px;
1914
+ color: var(--text);
1915
+ border-left: 3px solid var(--primary);
1615
1916
  }
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';
@@ -2769,6 +2780,7 @@
2769
2780
  const isTimelinePage = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
2770
2781
  const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
2771
2782
  const isGraphPage = document.body && document.body.dataset && document.body.dataset.page === 'graph';
2783
+ const isKanbanPage = document.body && document.body.dataset && document.body.dataset.page === 'kanban';
2772
2784
 
2773
2785
  // Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
2774
2786
  (async () => {
@@ -2819,6 +2831,11 @@
2819
2831
  return;
2820
2832
  }
2821
2833
 
2834
+ if (isKanbanPage) {
2835
+ await loadKanban();
2836
+ return;
2837
+ }
2838
+
2822
2839
  // If workspace isn't initialized yet, auto-init (reduces clicks)
2823
2840
  try {
2824
2841
  if (defaults && defaults.workspaceOk === false) {
@@ -2847,16 +2864,20 @@
2847
2864
  if (typeof saveAndPlan === 'function') saveAndPlan();
2848
2865
  return;
2849
2866
  }
2850
- // Ctrl/Cmd+K: Focus search/input on current page
2867
+ // Ctrl/Cmd+K: Open quick-add modal
2851
2868
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
2852
2869
  e.preventDefault();
2853
- const target = $('inboxText') || $('reportsFilter') || $('projectsFilter') || $('timelineFilter');
2854
- if (target) target.focus();
2870
+ openQuickAdd();
2855
2871
  return;
2856
2872
  }
2857
- // Escape: Blur active element
2858
- if (e.key === 'Escape' && document.activeElement) {
2859
- document.activeElement.blur();
2873
+ // Escape: Close quick-add modal or blur active element
2874
+ if (e.key === 'Escape') {
2875
+ const overlay = $('quickAddOverlay');
2876
+ if (overlay && overlay.style.display !== 'none') {
2877
+ closeQuickAdd();
2878
+ return;
2879
+ }
2880
+ if (document.activeElement) document.activeElement.blur();
2860
2881
  }
2861
2882
  });
2862
2883
 
@@ -2912,4 +2933,282 @@
2912
2933
  window.askFreya = askFreya;
2913
2934
  window.askFreyaInline = askFreyaInline;
2914
2935
  window.askFreyaFromInput = askFreyaFromInput;
2936
+
2937
+ /* ── Quick-Add Modal ── */
2938
+ function openQuickAdd() {
2939
+ const overlay = $('quickAddOverlay');
2940
+ if (!overlay) return;
2941
+ overlay.style.display = 'flex';
2942
+ const desc = $('qaDesc');
2943
+ if (desc) { desc.value = ''; desc.focus(); }
2944
+ var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
2945
+ var pri = $('qaPriority'); if (pri) pri.value = '';
2946
+ var slug = $('qaSlug'); if (slug) slug.value = '';
2947
+ var due = $('qaDue'); if (due) due.value = '';
2948
+ }
2949
+
2950
+ function closeQuickAdd() {
2951
+ var overlay = $('quickAddOverlay');
2952
+ if (overlay) overlay.style.display = 'none';
2953
+ }
2954
+
2955
+ async function submitQuickAdd() {
2956
+ var desc = $('qaDesc');
2957
+ var text = desc ? desc.value.trim() : '';
2958
+ if (!text) { showToast('err', 'Descricao obrigatoria'); return; }
2959
+
2960
+ var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
2961
+ var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
2962
+ var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
2963
+ var due = $('qaDue'); var dueVal = due ? due.value : '';
2964
+
2965
+ var body = { dir: dirOrDefault(), description: text, category: catVal };
2966
+ if (priVal) body.priority = priVal;
2967
+ if (slugVal) body.projectSlug = slugVal;
2968
+ if (dueVal) body.dueDate = dueVal;
2969
+
2970
+ try {
2971
+ await api('/api/tasks/create', body);
2972
+ closeQuickAdd();
2973
+ showToast('ok', 'Task criada');
2974
+ if (isKanbanPage) await loadKanban();
2975
+ else await refreshToday();
2976
+ } catch (e) {
2977
+ showToast('err', 'Erro ao criar task');
2978
+ }
2979
+ }
2980
+
2981
+ window.openQuickAdd = openQuickAdd;
2982
+ window.closeQuickAdd = closeQuickAdd;
2983
+ window.submitQuickAdd = submitQuickAdd;
2984
+
2985
+ /* ── Delta Banner ── */
2986
+ async function loadDelta() {
2987
+ var el = $('kanbanDelta') || $('deltaBanner');
2988
+ if (!el) return;
2989
+ try {
2990
+ var res = await api('/api/summary/delta', { dir: dirOrDefault() });
2991
+ if (!res || !res.delta) { el.style.display = 'none'; return; }
2992
+ var d = res.delta;
2993
+ var parts = [];
2994
+ if (d.completedTasks > 0) parts.push(d.completedTasks + ' concluida(s)');
2995
+ if (d.resolvedBlockers > 0) parts.push(d.resolvedBlockers + ' blocker(s) resolvido(s)');
2996
+ if (d.newTasks > 0) parts.push(d.newTasks + ' nova(s)');
2997
+ if (d.newBlockers > 0) parts.push(d.newBlockers + ' novo(s) blocker(s)');
2998
+ if (d.overdueTasks > 0) parts.push(d.overdueTasks + ' atrasada(s)');
2999
+ if (parts.length === 0) { el.style.display = 'none'; return; }
3000
+ el.style.display = 'flex';
3001
+ el.className = 'delta-banner';
3002
+ el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>'
3003
+ + '<span>Ultimas 24h: ' + escapeHtml(parts.join(' \u00b7 ')) + '</span>';
3004
+ } catch { el.style.display = 'none'; }
3005
+ }
3006
+
3007
+ /* ── Kanban Board ── */
3008
+ var _kanbanData = { tasks: [], blockers: [] };
3009
+
3010
+ async function loadKanban() {
3011
+ try {
3012
+ var res = await api('/api/tasks/kanban', { dir: dirOrDefault() });
3013
+ if (!res || !res.ok) return;
3014
+ _kanbanData.tasks = res.tasks || [];
3015
+ _kanbanData.blockers = res.blockers || [];
3016
+ populateKanbanProjects();
3017
+ renderKanban();
3018
+ renderKanbanBlockers();
3019
+ loadDelta();
3020
+ } catch (e) {
3021
+ showToast('err', 'Erro ao carregar kanban');
3022
+ }
3023
+ }
3024
+
3025
+ function populateKanbanProjects() {
3026
+ var sel = $('kanbanFilterProject');
3027
+ if (!sel) return;
3028
+ var slugs = new Set();
3029
+ _kanbanData.tasks.forEach(function(t) { if (t.projectSlug) slugs.add(t.projectSlug); });
3030
+ _kanbanData.blockers.forEach(function(b) { if (b.projectSlug) slugs.add(b.projectSlug); });
3031
+ var current = sel.value;
3032
+ sel.innerHTML = '<option value="">Todos os projetos</option>';
3033
+ Array.from(slugs).sort().forEach(function(s) {
3034
+ var opt = document.createElement('option');
3035
+ opt.value = s; opt.textContent = s;
3036
+ sel.appendChild(opt);
3037
+ });
3038
+ sel.value = current;
3039
+ }
3040
+
3041
+ function filterKanban() { renderKanban(); renderKanbanBlockers(); }
3042
+
3043
+ function getFilteredTasks() {
3044
+ var sel = $('kanbanFilterProject');
3045
+ var filter = sel ? sel.value : '';
3046
+ if (!filter) return _kanbanData.tasks;
3047
+ return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
3048
+ }
3049
+
3050
+ function renderKanban() {
3051
+ var tasks = getFilteredTasks();
3052
+ var today = new Date().toISOString().slice(0, 10);
3053
+ var sevenAgo = new Date(Date.now() - 7 * 86400000).toISOString();
3054
+
3055
+ var cols = {
3056
+ DO_NOW: [], SCHEDULE: [], DELEGATE: [], COMPLETED: []
3057
+ };
3058
+
3059
+ tasks.forEach(function(t) {
3060
+ if (t.status === 'COMPLETED') {
3061
+ if (t.completedAt && t.completedAt >= sevenAgo) cols.COMPLETED.push(t);
3062
+ } else if (t.status === 'PENDING' && cols[t.category]) {
3063
+ cols[t.category].push(t);
3064
+ }
3065
+ });
3066
+
3067
+ var idMap = { DO_NOW: 'colDoNow', SCHEDULE: 'colSchedule', DELEGATE: 'colDelegate', COMPLETED: 'colDone' };
3068
+ var countMap = { DO_NOW: 'countDoNow', SCHEDULE: 'countSchedule', DELEGATE: 'countDelegate', COMPLETED: 'countDone' };
3069
+
3070
+ Object.keys(cols).forEach(function(cat) {
3071
+ var el = $(idMap[cat]);
3072
+ var countEl = $(countMap[cat]);
3073
+ if (countEl) countEl.textContent = cols[cat].length;
3074
+ if (!el) return;
3075
+ el.innerHTML = '';
3076
+
3077
+ cols[cat].forEach(function(t) {
3078
+ var card = document.createElement('div');
3079
+ card.className = 'kanban-card';
3080
+ card.draggable = (cat !== 'COMPLETED');
3081
+ card.dataset.taskId = t.id;
3082
+ card.dataset.category = cat;
3083
+
3084
+ var pc = priColor(t.priority);
3085
+ var isOverdue = t.dueDate && t.dueDate < today && t.status === 'PENDING';
3086
+
3087
+ var html = '<div class="kanban-card-header">'
3088
+ + '<span class="kanban-pri-dot" style="background:' + pc.dot + ';" title="' + escapeHtml(pc.label) + '"></span>'
3089
+ + '<span class="kanban-card-desc">' + escapeHtml(t.description || '') + '</span>'
3090
+ + '</div>';
3091
+
3092
+ var meta = [];
3093
+ if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
3094
+ if (t.dueDate) {
3095
+ var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
3096
+ meta.push('<span class="' + dueCls + '">' + escapeHtml(t.dueDate) + '</span>');
3097
+ }
3098
+ if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
3099
+
3100
+ if (cat !== 'COMPLETED') {
3101
+ html += '<div class="kanban-card-actions">'
3102
+ + '<button class="kanban-action-btn complete-btn" title="Concluir">\u2713</button>'
3103
+ + '<button class="kanban-action-btn edit-btn" title="Editar">\u270E</button>'
3104
+ + '</div>';
3105
+ }
3106
+
3107
+ card.innerHTML = html;
3108
+
3109
+ // Drag events
3110
+ if (cat !== 'COMPLETED') {
3111
+ card.addEventListener('dragstart', function(e) {
3112
+ e.dataTransfer.setData('text/plain', t.id);
3113
+ e.dataTransfer.effectAllowed = 'move';
3114
+ card.classList.add('dragging');
3115
+ });
3116
+ card.addEventListener('dragend', function() {
3117
+ card.classList.remove('dragging');
3118
+ });
3119
+
3120
+ // Complete button
3121
+ var completeBtn = card.querySelector('.complete-btn');
3122
+ if (completeBtn) {
3123
+ completeBtn.onclick = async function() {
3124
+ try {
3125
+ await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
3126
+ showToast('ok', 'Concluida');
3127
+ await loadKanban();
3128
+ } catch { showToast('err', 'Falhou'); }
3129
+ };
3130
+ }
3131
+
3132
+ // Edit button - inline edit via prompt
3133
+ var editBtn = card.querySelector('.edit-btn');
3134
+ if (editBtn) {
3135
+ editBtn.onclick = async function() {
3136
+ var newCat = prompt('Categoria (DO_NOW|SCHEDULE|DELEGATE):', cat);
3137
+ if (!newCat) return;
3138
+ var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
3139
+ if (newSlug === null) return;
3140
+ var newDue = prompt('Due date (YYYY-MM-DD):', t.dueDate || '');
3141
+ if (newDue === null) return;
3142
+ try {
3143
+ await api('/api/tasks/update', {
3144
+ dir: dirOrDefault(), id: t.id,
3145
+ patch: { category: newCat, projectSlug: newSlug, dueDate: newDue || null }
3146
+ });
3147
+ showToast('ok', 'Atualizada');
3148
+ await loadKanban();
3149
+ } catch { showToast('err', 'Falhou'); }
3150
+ };
3151
+ }
3152
+ }
3153
+
3154
+ el.appendChild(card);
3155
+ });
3156
+
3157
+ // Drop zone
3158
+ if (cat !== 'COMPLETED') {
3159
+ el.addEventListener('dragover', function(e) {
3160
+ e.preventDefault();
3161
+ e.dataTransfer.dropEffect = 'move';
3162
+ el.classList.add('drag-over');
3163
+ });
3164
+ el.addEventListener('dragleave', function() {
3165
+ el.classList.remove('drag-over');
3166
+ });
3167
+ el.addEventListener('drop', async function(e) {
3168
+ e.preventDefault();
3169
+ el.classList.remove('drag-over');
3170
+ var taskId = e.dataTransfer.getData('text/plain');
3171
+ if (!taskId) return;
3172
+ try {
3173
+ await api('/api/tasks/update', {
3174
+ dir: dirOrDefault(), id: taskId,
3175
+ patch: { category: cat }
3176
+ });
3177
+ showToast('ok', 'Movida para ' + cat);
3178
+ await loadKanban();
3179
+ } catch { showToast('err', 'Falhou'); }
3180
+ });
3181
+ }
3182
+ });
3183
+ }
3184
+
3185
+ function renderKanbanBlockers() {
3186
+ var wrap = $('kanbanBlockers');
3187
+ var list = $('kanbanBlockersList');
3188
+ if (!wrap || !list) return;
3189
+
3190
+ var sel = $('kanbanFilterProject');
3191
+ var filter = sel ? sel.value : '';
3192
+ var blockers = _kanbanData.blockers;
3193
+ if (filter) blockers = blockers.filter(function(b) { return b.projectSlug === filter; });
3194
+
3195
+ if (blockers.length === 0) { wrap.style.display = 'none'; return; }
3196
+ wrap.style.display = 'block';
3197
+ list.innerHTML = '';
3198
+
3199
+ blockers.forEach(function(b) {
3200
+ var card = document.createElement('div');
3201
+ card.className = 'kanban-blocker-card';
3202
+ var color = sevColor(b.severity);
3203
+ card.innerHTML = '<span class="kanban-blocker-sev" style="background:' + color + '22; color:' + color + '; border-color:' + color + '44;">' + escapeHtml(b.severity || 'BLOCKER') + '</span>'
3204
+ + '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
3205
+ + (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
3206
+ + (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
3207
+ list.appendChild(card);
3208
+ });
3209
+ }
3210
+
3211
+ window.loadKanban = loadKanban;
3212
+ window.filterKanban = filterKanban;
3213
+ window.loadDelta = loadDelta;
2915
3214
  })();