@cccarv82/freya 2.15.0 → 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.css CHANGED
@@ -1682,6 +1682,11 @@ textarea:focus {
1682
1682
  background: var(--chip);
1683
1683
  }
1684
1684
 
1685
+ /* Compact kanban on dashboard (not standalone /kanban page) */
1686
+ body:not([data-page="kanban"]) .kanban-col-body {
1687
+ max-height: 400px;
1688
+ }
1689
+
1685
1690
  .kanban-card {
1686
1691
  background: var(--paper);
1687
1692
  border: 1px solid var(--line2);
@@ -1901,6 +1906,48 @@ textarea:focus {
1901
1906
  align-items: center;
1902
1907
  }
1903
1908
 
1909
+ .qa-date-wrap {
1910
+ position: relative;
1911
+ width: 160px;
1912
+ display: flex;
1913
+ align-items: center;
1914
+ }
1915
+ .qa-date-text {
1916
+ padding-right: 32px !important;
1917
+ }
1918
+ .qa-date-hidden {
1919
+ position: absolute;
1920
+ top: 0;
1921
+ left: 0;
1922
+ width: 1px;
1923
+ height: 1px;
1924
+ opacity: 0;
1925
+ pointer-events: none;
1926
+ overflow: hidden;
1927
+ clip: rect(0,0,0,0);
1928
+ border: 0;
1929
+ padding: 0;
1930
+ margin: -1px;
1931
+ }
1932
+ .qa-date-btn {
1933
+ position: absolute;
1934
+ right: 4px;
1935
+ top: 50%;
1936
+ transform: translateY(-50%);
1937
+ background: none;
1938
+ border: none;
1939
+ cursor: pointer;
1940
+ font-size: 16px;
1941
+ padding: 2px 4px;
1942
+ line-height: 1;
1943
+ color: var(--muted);
1944
+ opacity: 0.8;
1945
+ transition: opacity 0.15s;
1946
+ }
1947
+ .qa-date-btn:hover {
1948
+ opacity: 1;
1949
+ }
1950
+
1904
1951
  /* ── Delta Banner ── */
1905
1952
  .delta-banner {
1906
1953
  display: flex;
@@ -1913,4 +1960,110 @@ textarea:focus {
1913
1960
  font-size: 12px;
1914
1961
  color: var(--text);
1915
1962
  border-left: 3px solid var(--primary);
1916
- }
1963
+ }
1964
+
1965
+ /* ── Detail Panel (click-to-view on kanban cards) ── */
1966
+ .detail-panel-overlay {
1967
+ position: fixed;
1968
+ inset: 0;
1969
+ background: rgba(0,0,0,0.55);
1970
+ display: flex;
1971
+ align-items: center;
1972
+ justify-content: center;
1973
+ z-index: 10000;
1974
+ animation: fadeIn 0.15s ease;
1975
+ }
1976
+ .detail-panel {
1977
+ background: var(--paper);
1978
+ border: 1px solid var(--line2);
1979
+ border-radius: 12px;
1980
+ width: 520px;
1981
+ max-width: 95vw;
1982
+ max-height: 85vh;
1983
+ overflow-y: auto;
1984
+ padding: 20px 24px;
1985
+ box-shadow: 0 12px 40px rgba(0,0,0,0.4);
1986
+ }
1987
+ .detail-panel-header {
1988
+ display: flex;
1989
+ align-items: flex-start;
1990
+ justify-content: space-between;
1991
+ gap: 12px;
1992
+ margin-bottom: 16px;
1993
+ padding-bottom: 12px;
1994
+ border-bottom: 1px solid var(--border);
1995
+ }
1996
+ .detail-panel-title {
1997
+ font-size: 16px;
1998
+ font-weight: 700;
1999
+ color: var(--text);
2000
+ word-break: break-word;
2001
+ }
2002
+ .detail-panel-close {
2003
+ background: none;
2004
+ border: none;
2005
+ color: var(--muted);
2006
+ font-size: 22px;
2007
+ cursor: pointer;
2008
+ padding: 0 4px;
2009
+ line-height: 1;
2010
+ flex-shrink: 0;
2011
+ }
2012
+ .detail-panel-close:hover {
2013
+ color: var(--text);
2014
+ }
2015
+ .detail-panel-grid {
2016
+ display: grid;
2017
+ grid-template-columns: 120px 1fr;
2018
+ gap: 6px 12px;
2019
+ margin-bottom: 12px;
2020
+ }
2021
+ .detail-row-label {
2022
+ font-size: 11px;
2023
+ font-weight: 700;
2024
+ text-transform: uppercase;
2025
+ letter-spacing: 0.5px;
2026
+ color: var(--muted);
2027
+ padding: 3px 0;
2028
+ }
2029
+ .detail-row-value {
2030
+ font-size: 13px;
2031
+ color: var(--text);
2032
+ padding: 3px 0;
2033
+ word-break: break-word;
2034
+ font-family: var(--mono);
2035
+ }
2036
+ .detail-panel-section {
2037
+ margin-top: 12px;
2038
+ padding-top: 12px;
2039
+ border-top: 1px solid var(--border);
2040
+ }
2041
+ .detail-panel-section-title {
2042
+ font-size: 11px;
2043
+ font-weight: 700;
2044
+ text-transform: uppercase;
2045
+ letter-spacing: 0.5px;
2046
+ color: var(--muted);
2047
+ margin-bottom: 8px;
2048
+ }
2049
+ .detail-panel-comment {
2050
+ font-size: 12px;
2051
+ color: var(--text);
2052
+ padding: 6px 10px;
2053
+ background: var(--bg2);
2054
+ border-radius: 6px;
2055
+ margin-bottom: 4px;
2056
+ display: flex;
2057
+ flex-direction: column;
2058
+ gap: 2px;
2059
+ }
2060
+ .detail-panel-comment-date {
2061
+ font-size: 10px;
2062
+ color: var(--muted);
2063
+ font-family: var(--mono);
2064
+ }
2065
+ @keyframes fadeIn {
2066
+ from { opacity: 0; }
2067
+ to { opacity: 1; }
2068
+ }
2069
+
package/cli/web-ui.js CHANGED
@@ -1545,229 +1545,9 @@
1545
1545
  return '#94a3b8';
1546
1546
  }
1547
1547
 
1548
- function renderSwimlanes(tasks, blockers) {
1549
- const el = $('swimlaneContainer');
1550
- if (!el) return;
1551
- el.innerHTML = '';
1552
-
1553
- // Update top-level summary chips
1554
- const chips = $('focusSummaryChips');
1555
- if (chips) {
1556
- chips.innerHTML = '';
1557
- if (blockers.length > 0) {
1558
- const bc = document.createElement('span');
1559
- 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);';
1560
- bc.textContent = blockers.length + ' blocker' + (blockers.length > 1 ? 's' : '');
1561
- chips.appendChild(bc);
1562
- }
1563
- if (tasks.length > 0) {
1564
- const tc = document.createElement('span');
1565
- 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);';
1566
- tc.textContent = tasks.length + ' task' + (tasks.length > 1 ? 's' : '');
1567
- chips.appendChild(tc);
1568
- }
1569
- }
1570
-
1571
- // Update progress bar (tasks completed today vs total — we show pending count)
1572
- const progWrap = $('focusProgressWrap');
1573
- const progBar = $('focusProgressBar');
1574
- const progLabel = $('focusProgressLabel');
1575
- if (progWrap) {
1576
- if (tasks.length > 0 || blockers.length > 0) {
1577
- progWrap.style.display = 'flex';
1578
- // We only have pending tasks here; show blockers impact
1579
- const blocker_pct = blockers.length === 0 ? 100 : Math.max(10, Math.round((1 - blockers.length / Math.max(tasks.length + blockers.length, 1)) * 100));
1580
- if (progBar) progBar.style.width = blocker_pct + '%';
1581
- if (progBar) progBar.style.background = blockers.length > 0 ? '#f97316' : 'var(--accent)';
1582
- if (progLabel) progLabel.textContent = blockers.length > 0 ? blockers.length + ' blocking' : 'tudo livre';
1583
- } else {
1584
- progWrap.style.display = 'none';
1585
- }
1586
- }
1587
-
1588
- const groups = {};
1589
- const addGroup = (slug) => {
1590
- const key = slug || 'Global / Sem Projeto';
1591
- if (!groups[key]) groups[key] = { tasks: [], blockers: [] };
1592
- return key;
1593
- };
1594
-
1595
- for (const t of tasks) groups[addGroup(t.projectSlug)].tasks.push(t);
1596
- for (const b of blockers) groups[addGroup(b.projectSlug)].blockers.push(b);
1597
-
1598
- const sortedKeys = Object.keys(groups).sort((a, b) => {
1599
- if (a === 'Global / Sem Projeto') return 1;
1600
- if (b === 'Global / Sem Projeto') return -1;
1601
- return a.localeCompare(b);
1602
- });
1603
- sortedKeys.sort((a, b) => groups[b].blockers.length - groups[a].blockers.length);
1604
-
1605
- if (sortedKeys.length === 0) {
1606
- const empty = document.createElement('div');
1607
- empty.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 24px; gap:8px; flex:1;';
1608
- empty.innerHTML = '<div style="font-size:28px; line-height:1;">✅</div>'
1609
- + '<div style="font-size:14px; font-weight:600; color:var(--text);">Dia limpo!</div>'
1610
- + '<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>';
1611
- el.appendChild(empty);
1612
- return;
1613
- }
1614
-
1615
- const createTaskRow = (t) => {
1616
- const row = document.createElement('div');
1617
- const pc = priColor(t.priority);
1618
- 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;';
1619
- row.onmouseover = () => row.style.background = 'var(--paper)';
1620
- row.onmouseout = () => row.style.background = 'var(--bg)';
1621
- row.innerHTML =
1622
- '<div style="display:flex; align-items:center; gap:10px; min-width:0; flex:1;">'
1623
- + '<div style="width:8px; height:8px; border-radius:50%; background:' + pc.dot + '; flex-shrink:0;" title="Prioridade: ' + escapeHtml(pc.label) + '"></div>'
1624
- + '<div style="min-width:0;">'
1625
- + '<div style="font-weight:600; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">' + escapeHtml(t.description || '') + '</div>'
1626
- + '<div style="font-size:11px; color:var(--muted); margin-top:2px;">'
1627
- + escapeHtml(pc.label)
1628
- + (t.category ? ' · <span style="font-family:var(--mono);">' + escapeHtml(t.category) + '</span>' : '')
1629
- + '</div>'
1630
- + '</div></div>'
1631
- + '<div class="task-actions" style="display:flex; gap:6px; flex-shrink:0; align-items:center;">'
1632
- + '<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" title="Marcar como concluída">✓ Concluir</button>'
1633
- + '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">✎</button>'
1634
- + '</div>';
1635
- row.querySelector('.edit-btn').onclick = () => editTask(t);
1636
-
1637
- var attachCompleteHandler = function() {
1638
- var cBtn = row.querySelector('.complete-btn');
1639
- if (!cBtn) return;
1640
- cBtn.onclick = function() {
1641
- var actionsDiv = row.querySelector('.task-actions');
1642
- actionsDiv.innerHTML =
1643
- '<input type="text" class="comment-input" placeholder="Comentario (opcional)" '
1644
- + 'style="font-size:11px; padding:3px 8px; background:var(--bg); border:1px solid var(--border); '
1645
- + 'color:var(--text); width:180px; outline:none; font-family:var(--mono);" />'
1646
- + '<button class="btn small confirm-btn" type="button" style="padding:3px 10px; font-size:11px; '
1647
- + 'background:var(--accent); color:#000; font-weight:700;">Confirmar</button>'
1648
- + '<button class="btn small cancel-btn" type="button" style="padding:3px 8px; font-size:11px; '
1649
- + 'color:var(--muted);">\u2715</button>';
1650
-
1651
- var input = actionsDiv.querySelector('.comment-input');
1652
- input.focus();
1653
-
1654
- var doComplete = async function() {
1655
- var comment = input.value.trim();
1656
- actionsDiv.querySelector('.confirm-btn').disabled = true;
1657
- actionsDiv.querySelector('.confirm-btn').textContent = '\u2026';
1658
- try {
1659
- setPill('run', 'concluindo\u2026');
1660
- var body = { dir: dirOrDefault(), id: t.id };
1661
- if (comment) body.comment = comment;
1662
- await api('/api/tasks/complete', body);
1663
- row.style.opacity = '0.4';
1664
- row.style.pointerEvents = 'none';
1665
- await refreshToday();
1666
- setPill('ok', 'conclu\u00edda');
1667
- setTimeout(function() { setPill('ok', 'pronto'); }, 800);
1668
- } catch (e) {
1669
- actionsDiv.querySelector('.confirm-btn').disabled = false;
1670
- actionsDiv.querySelector('.confirm-btn').textContent = 'Confirmar';
1671
- setPill('err', 'falhou');
1672
- }
1673
- };
1674
-
1675
- actionsDiv.querySelector('.confirm-btn').onclick = doComplete;
1676
- input.onkeydown = function(e) { if (e.key === 'Enter') doComplete(); };
1677
- actionsDiv.querySelector('.cancel-btn').onclick = function() {
1678
- actionsDiv.innerHTML =
1679
- '<button class="btn small complete-btn" type="button" style="padding:3px 10px; font-size:11px;" '
1680
- + 'title="Marcar como conclu\u00edda">\u2713 Concluir</button>'
1681
- + '<button class="btn small edit-btn" type="button" style="padding:3px 8px; font-size:11px;">\u270E</button>';
1682
- actionsDiv.querySelector('.edit-btn').onclick = function() { editTask(t); };
1683
- attachCompleteHandler();
1684
- };
1685
- };
1686
- };
1687
- attachCompleteHandler();
1688
- return row;
1689
- };
1690
-
1691
- const createBlockerRow = (b) => {
1692
- const row = document.createElement('div');
1693
- const sev = String(b.severity || '').toUpperCase();
1694
- const color = sevColor(sev);
1695
- const when = fmtWhen(new Date(b.createdAt || Date.now()).getTime());
1696
- 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;';
1697
- row.onmouseover = () => row.style.background = 'rgba(239,68,68,0.08)';
1698
- row.onmouseout = () => row.style.background = 'rgba(239,68,68,0.04)';
1699
- row.innerHTML =
1700
- '<div style="display:flex; align-items:flex-start; gap:10px; min-width:0; flex:1;">'
1701
- + '<div style="flex-shrink:0; margin-top:1px;">'
1702
- + '<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>'
1703
- + '</div>'
1704
- + '<div style="min-width:0; flex:1;">'
1705
- + '<div style="font-weight:600; font-size:13px; color:var(--text);">' + escapeHtml(b.title || '') + '</div>'
1706
- + '<div style="font-size:11px; color:var(--muted); margin-top:3px;">🕐 ' + escapeHtml(when) + '</div>'
1707
- + '</div></div>'
1708
- + '<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>';
1709
- row.querySelector('.edit-btn').onclick = () => editBlocker(b);
1710
- return row;
1711
- };
1712
-
1713
- for (const key of sortedKeys) {
1714
- const g = groups[key];
1715
- const bCount = g.blockers.length;
1716
- const tCount = g.tasks.length;
1717
- const hasBlockers = bCount > 0;
1718
-
1719
- const swimlane = document.createElement('div');
1720
- swimlane.style.cssText = 'border-bottom:1px solid var(--border); overflow:hidden;';
1721
-
1722
- const head = document.createElement('div');
1723
- head.style.cssText = 'display:flex; justify-content:space-between; align-items:center; cursor:pointer; padding:9px 16px; background:var(--bg2); transition:background 0.15s;'
1724
- + (hasBlockers ? 'border-left:3px solid #f97316;' : 'border-left:3px solid transparent;');
1725
- head.onmouseover = () => head.style.background = 'var(--paper2)';
1726
- head.onmouseout = () => head.style.background = 'var(--bg2)';
1727
-
1728
- head.innerHTML = `
1729
- <div style="display:flex; align-items:center; gap:8px; min-width:0;">
1730
- <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>
1731
- <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>
1732
- </div>
1733
- <div style="display:flex; gap:5px; align-items:center; flex-shrink:0;">
1734
- ${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>` : ''}
1735
- ${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>` : ''}
1736
- </div>
1737
- `;
1738
-
1739
- const body = document.createElement('div');
1740
- body.style.cssText = 'display:flex; flex-direction:column; background:var(--bg);';
1741
-
1742
- for (const b of g.blockers) body.appendChild(createBlockerRow(b));
1743
- for (const t of g.tasks) body.appendChild(createTaskRow(t));
1744
-
1745
- let isOpen = true;
1746
- head.onclick = () => {
1747
- isOpen = !isOpen;
1748
- body.style.display = isOpen ? 'flex' : 'none';
1749
- const svg = head.querySelector('svg');
1750
- if (svg) svg.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
1751
- };
1752
-
1753
- swimlane.appendChild(head);
1754
- swimlane.appendChild(body);
1755
- el.appendChild(swimlane);
1756
- }
1757
- }
1758
-
1548
+ // refreshToday now delegates to loadKanban (dashboard uses embedded kanban board)
1759
1549
  async function refreshToday() {
1760
- showSkeleton('swimlaneContainer');
1761
- try {
1762
- const [t, b] = await Promise.all([
1763
- api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 50 }),
1764
- api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 50 })
1765
- ]);
1766
- renderSwimlanes((t && t.tasks) || [], (b && b.blockers) || []);
1767
- refreshBlockersInsights();
1768
- } catch (e) {
1769
- // keep silent in background refresh
1770
- }
1550
+ await loadKanban();
1771
1551
  }
1772
1552
 
1773
1553
  function renderBlockersInsights(payload) {
@@ -2935,16 +2715,45 @@
2935
2715
  window.askFreyaFromInput = askFreyaFromInput;
2936
2716
 
2937
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
+
2938
2745
  function openQuickAdd() {
2939
2746
  const overlay = $('quickAddOverlay');
2940
2747
  if (!overlay) return;
2941
2748
  overlay.style.display = 'flex';
2749
+ wireQaDatePicker();
2942
2750
  const desc = $('qaDesc');
2943
2751
  if (desc) { desc.value = ''; desc.focus(); }
2944
2752
  var cat = $('qaCat'); if (cat) cat.value = 'DO_NOW';
2945
2753
  var pri = $('qaPriority'); if (pri) pri.value = '';
2946
2754
  var slug = $('qaSlug'); if (slug) slug.value = '';
2947
2755
  var due = $('qaDue'); if (due) due.value = '';
2756
+ var picker = $('qaDuePicker'); if (picker) picker.value = '';
2948
2757
  }
2949
2758
 
2950
2759
  function closeQuickAdd() {
@@ -2952,6 +2761,19 @@
2952
2761
  if (overlay) overlay.style.display = 'none';
2953
2762
  }
2954
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
+
2955
2777
  async function submitQuickAdd() {
2956
2778
  var desc = $('qaDesc');
2957
2779
  var text = desc ? desc.value.trim() : '';
@@ -2960,7 +2782,7 @@
2960
2782
  var cat = $('qaCat'); var catVal = cat ? cat.value : 'DO_NOW';
2961
2783
  var pri = $('qaPriority'); var priVal = pri ? pri.value : '';
2962
2784
  var slug = $('qaSlug'); var slugVal = slug ? slug.value.trim() : '';
2963
- var due = $('qaDue'); var dueVal = due ? due.value : '';
2785
+ var due = $('qaDue'); var dueVal = due ? parseDateBR(due.value) : '';
2964
2786
 
2965
2787
  var body = { dir: dirOrDefault(), description: text, category: catVal };
2966
2788
  if (priVal) body.priority = priVal;
@@ -2971,8 +2793,7 @@
2971
2793
  await api('/api/tasks/create', body);
2972
2794
  closeQuickAdd();
2973
2795
  showToast('ok', 'Task criada');
2974
- if (isKanbanPage) await loadKanban();
2975
- else await refreshToday();
2796
+ await loadKanban();
2976
2797
  } catch (e) {
2977
2798
  showToast('err', 'Erro ao criar task');
2978
2799
  }
@@ -3017,6 +2838,8 @@
3017
2838
  renderKanban();
3018
2839
  renderKanbanBlockers();
3019
2840
  loadDelta();
2841
+ // On dashboard, also refresh blockers insights panel
2842
+ if ($('blockersInsightsWrap')) refreshBlockersInsights();
3020
2843
  } catch (e) {
3021
2844
  showToast('err', 'Erro ao carregar kanban');
3022
2845
  }
@@ -3047,6 +2870,127 @@
3047
2870
  return _kanbanData.tasks.filter(function(t) { return t.projectSlug === filter; });
3048
2871
  }
3049
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
+
3050
2994
  function renderKanban() {
3051
2995
  var tasks = getFilteredTasks();
3052
2996
  var today = new Date().toISOString().slice(0, 10);
@@ -3093,7 +3037,7 @@
3093
3037
  if (t.projectSlug) meta.push('<span class="kanban-tag">' + escapeHtml(t.projectSlug) + '</span>');
3094
3038
  if (t.dueDate) {
3095
3039
  var dueCls = isOverdue ? 'kanban-due overdue' : 'kanban-due';
3096
- meta.push('<span class="' + dueCls + '">' + escapeHtml(t.dueDate) + '</span>');
3040
+ meta.push('<span class="' + dueCls + '">' + escapeHtml(fmtDateBR(t.dueDate)) + '</span>');
3097
3041
  }
3098
3042
  if (meta.length) html += '<div class="kanban-card-meta">' + meta.join('') + '</div>';
3099
3043
 
@@ -3106,6 +3050,17 @@
3106
3050
 
3107
3051
  card.innerHTML = html;
3108
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
+
3109
3064
  // Drag events
3110
3065
  if (cat !== 'COMPLETED') {
3111
3066
  card.addEventListener('dragstart', function(e) {
@@ -3137,8 +3092,17 @@
3137
3092
  if (!newCat) return;
3138
3093
  var newSlug = prompt('Projeto (slug):', t.projectSlug || '');
3139
3094
  if (newSlug === null) return;
3140
- var newDue = prompt('Due date (YYYY-MM-DD):', t.dueDate || '');
3141
- if (newDue === 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
+ }
3142
3106
  try {
3143
3107
  await api('/api/tasks/update', {
3144
3108
  dir: dirOrDefault(), id: t.id,
@@ -3204,6 +3168,8 @@
3204
3168
  + '<span class="kanban-blocker-title">' + escapeHtml(b.title || '') + '</span>'
3205
3169
  + (b.projectSlug ? '<span class="kanban-tag">' + escapeHtml(b.projectSlug) + '</span>' : '')
3206
3170
  + (b.owner ? '<span class="kanban-owner">' + escapeHtml(b.owner) + '</span>' : '');
3171
+ card.style.cursor = 'pointer';
3172
+ card.addEventListener('click', function() { showDetailPanel(b, 'blocker'); });
3207
3173
  list.appendChild(card);
3208
3174
  });
3209
3175
  }
package/cli/web.js CHANGED
@@ -1272,55 +1272,78 @@ function buildHtml(safeDefault, appVersion) {
1272
1272
  </div>
1273
1273
  </div>
1274
1274
 
1275
- <div class="centerHead">
1276
- <div>
1277
- <h1 style="margin:0">Seu dia em um painel</h1>
1278
- <div class="subtitle">Use o campo acima para capturar updates do dia (<b>Salvar &amp; Processar</b>) ou consultar o histórico (<b>Perguntar</b>). As respostas aparecem logo abaixo do input.</div>
1279
- </div>
1280
- <div class="statusLine">
1281
- <span class="small" id="last"></span>
1275
+ <!-- Kanban toolbar -->
1276
+ <div class="kanban-toolbar" style="display:flex; justify-content:space-between; align-items:center; padding:0 0 16px; gap:12px; flex-wrap:wrap;">
1277
+ <div style="display:flex; gap:8px; align-items:center;">
1278
+ <select id="kanbanFilterProject" class="kanban-filter" onchange="window.filterKanban()">
1279
+ <option value="">Todos os projetos</option>
1280
+ </select>
1281
+ <button class="btn small" type="button" onclick="window.loadKanban()">
1282
+ <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>
1283
+ Atualizar
1284
+ </button>
1285
+ <div class="statusLine" style="margin:0;">
1286
+ <span class="small" id="last"></span>
1287
+ </div>
1282
1288
  </div>
1289
+ <button class="btn primary small" type="button" onclick="window.openQuickAdd()">+ Nova Task</button>
1283
1290
  </div>
1284
1291
 
1285
- <section class="panel" style="display:flex; flex-direction:column; min-height:0; margin-bottom:16px;">
1286
- <!-- Header with live counters -->
1287
- <div class="panelHead" style="background: linear-gradient(90deg, var(--paper2), var(--paper)); border-left: 4px solid var(--accent); flex-shrink:0;">
1288
- <div style="display:flex; align-items:center; gap:10px;">
1289
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent); flex-shrink:0;"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
1290
- <b style="color: var(--text); font-size: 14px;">Foco de Hoje</b>
1291
- <div id="focusSummaryChips" style="display:flex; gap:5px; margin-left:4px;"></div>
1292
- </div>
1293
- <div class="stack">
1294
- <div id="focusProgressWrap" style="display:none; align-items:center; gap:6px; font-size:11px; color:var(--muted);">
1295
- <div style="width:80px; height:5px; background:var(--border); border-radius:3px; overflow:hidden;">
1296
- <div id="focusProgressBar" style="height:100%; background:var(--accent); border-radius:3px; transition:width 0.4s; width:0%"></div>
1297
- </div>
1298
- <span id="focusProgressLabel"></span>
1299
- </div>
1300
- <button class="btn small" type="button" onclick="refreshToday()">
1301
- <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>
1302
- Atualizar
1303
- </button>
1304
- </div>
1305
- </div>
1292
+ <!-- Delta banner -->
1293
+ <div id="kanbanDelta" style="display:none;"></div>
1306
1294
 
1307
- <!-- Scrollable swimlanes body -->
1308
- <div class="panelBody" style="flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:0; padding:0; min-height:0;">
1309
- <div id="swimlaneContainer" style="display:flex; flex-direction: column; gap: 0; flex:1;"></div>
1295
+ <!-- Kanban columns -->
1296
+ <div id="kanbanBoard" class="kanban-board">
1297
+ <div class="kanban-col" data-category="DO_NOW">
1298
+ <div class="kanban-col-head do-now">
1299
+ <span class="kanban-col-title">DO NOW</span>
1300
+ <span class="kanban-col-count" id="countDoNow">0</span>
1301
+ </div>
1302
+ <div class="kanban-col-body" id="colDoNow"></div>
1303
+ </div>
1304
+ <div class="kanban-col" data-category="SCHEDULE">
1305
+ <div class="kanban-col-head schedule">
1306
+ <span class="kanban-col-title">SCHEDULE</span>
1307
+ <span class="kanban-col-count" id="countSchedule">0</span>
1308
+ </div>
1309
+ <div class="kanban-col-body" id="colSchedule"></div>
1310
+ </div>
1311
+ <div class="kanban-col" data-category="DELEGATE">
1312
+ <div class="kanban-col-head delegate">
1313
+ <span class="kanban-col-title">DELEGATE</span>
1314
+ <span class="kanban-col-count" id="countDelegate">0</span>
1315
+ </div>
1316
+ <div class="kanban-col-body" id="colDelegate"></div>
1317
+ </div>
1318
+ <div class="kanban-col" data-category="COMPLETED">
1319
+ <div class="kanban-col-head done">
1320
+ <span class="kanban-col-title">DONE (7d)</span>
1321
+ <span class="kanban-col-count" id="countDone">0</span>
1310
1322
  </div>
1323
+ <div class="kanban-col-body" id="colDone"></div>
1324
+ </div>
1325
+ </div>
1311
1326
 
1312
- <!-- Insights strip (collapsed by default, expands when content is available) -->
1313
- <div id="blockersInsightsWrap" style="flex-shrink:0; border-top: 1px solid var(--border); display:none;">
1314
- <div style="display:flex; justify-content:space-between; align-items:center; padding: 8px 16px 4px;">
1315
- <div style="font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--muted);">
1316
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-1px;margin-right:4px"><path d="M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
1317
- Insights de Bloqueios
1318
- </div>
1319
- <button class="btn small" type="button" onclick="refreshBlockersInsights()" style="font-size:10px; padding:2px 6px;">↻</button>
1320
- </div>
1321
- <div id="blockersInsights" style="padding: 4px 16px 12px; font-size:12px; color:var(--muted);"></div>
1327
+ <!-- Blockers strip below kanban -->
1328
+ <div id="kanbanBlockers" style="margin-top:20px; display:none;">
1329
+ <div style="font-size:13px; font-weight:700; color:var(--text); margin-bottom:8px; display:flex; align-items:center; gap:6px;">
1330
+ <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>
1331
+ Blockers Ativos
1332
+ </div>
1333
+ <div id="kanbanBlockersList" class="kanban-blockers-list"></div>
1334
+ </div>
1335
+
1336
+ <!-- Insights de bloqueios -->
1337
+ <div id="blockersInsightsWrap" style="margin-top:16px; border-top: 1px solid var(--border); display:none;">
1338
+ <div style="display:flex; justify-content:space-between; align-items:center; padding: 8px 0 4px;">
1339
+ <div style="font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--muted);">
1340
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-1px;margin-right:4px"><path d="M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
1341
+ Insights de Bloqueios
1322
1342
  </div>
1323
- </section>
1343
+ <button class="btn small" type="button" onclick="refreshBlockersInsights()" style="font-size:10px; padding:2px 6px;">↻</button>
1344
+ </div>
1345
+ <div id="blockersInsights" style="padding: 4px 0 12px; font-size:12px; color:var(--muted);"></div>
1346
+ </div>
1324
1347
 
1325
1348
  </div>
1326
1349
  </main>
@@ -1352,7 +1375,11 @@ function buildHtml(safeDefault, appVersion) {
1352
1375
  </div>
1353
1376
  <div class="qa-row">
1354
1377
  <input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
1355
- <input id="qaDue" type="date" class="qa-input" style="width:160px;" />
1378
+ <div class="qa-date-wrap">
1379
+ <input id="qaDue" type="text" class="qa-input qa-date-text" placeholder="dd/mm/aaaa" maxlength="10" />
1380
+ <input id="qaDuePicker" type="date" class="qa-date-hidden" tabindex="-1" />
1381
+ <button type="button" class="qa-date-btn" onclick="var p=this.parentElement.querySelector('#qaDuePicker'); p.showPicker ? p.showPicker() : p.click();" title="Abrir calendario">&#128197;</button>
1382
+ </div>
1356
1383
  </div>
1357
1384
  <div class="qa-row" style="justify-content:flex-end;">
1358
1385
  <button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
@@ -4256,7 +4283,10 @@ async function cmdWeb({ port, dir, open, dev }) {
4256
4283
  dueDate: t.due_date || null,
4257
4284
  projectSlug: t.project_slug,
4258
4285
  priority: meta.priority,
4259
- streamSlug: meta.streamSlug
4286
+ streamSlug: meta.streamSlug,
4287
+ comments: meta.comments || [],
4288
+ source: meta.source || null,
4289
+ metadata: meta
4260
4290
  };
4261
4291
  });
4262
4292
 
@@ -4265,15 +4295,23 @@ async function cmdWeb({ port, dir, open, dev }) {
4265
4295
  ORDER BY
4266
4296
  CASE severity WHEN 'CRITICAL' THEN 0 WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 WHEN 'LOW' THEN 3 ELSE 9 END ASC,
4267
4297
  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
- }));
4298
+ `).all().map(b => {
4299
+ let meta = {};
4300
+ try { meta = b.metadata ? JSON.parse(b.metadata) : {}; } catch { meta = {}; }
4301
+ return {
4302
+ id: b.id,
4303
+ title: b.title,
4304
+ severity: b.severity,
4305
+ status: b.status,
4306
+ projectSlug: b.project_slug,
4307
+ owner: b.owner,
4308
+ nextAction: b.next_action,
4309
+ createdAt: b.created_at,
4310
+ resolvedAt: b.resolved_at,
4311
+ source: meta.source || null,
4312
+ metadata: meta
4313
+ };
4314
+ });
4277
4315
 
4278
4316
  return safeJson(res, 200, { ok: true, tasks, blockers: openBlockers });
4279
4317
  }
@@ -4675,7 +4713,11 @@ function buildKanbanHtml(safeDefault, appVersion) {
4675
4713
  </div>
4676
4714
  <div class="qa-row">
4677
4715
  <input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
4678
- <input id="qaDue" type="date" class="qa-input" style="width:160px;" />
4716
+ <div class="qa-date-wrap">
4717
+ <input id="qaDue" type="text" class="qa-input qa-date-text" placeholder="dd/mm/aaaa" maxlength="10" />
4718
+ <input id="qaDuePicker" type="date" class="qa-date-hidden" tabindex="-1" />
4719
+ <button type="button" class="qa-date-btn" onclick="var p=this.parentElement.querySelector('#qaDuePicker'); p.showPicker ? p.showPicker() : p.click();" title="Abrir calendario">&#128197;</button>
4720
+ </div>
4679
4721
  </div>
4680
4722
  <div class="qa-row" style="justify-content:flex-end;">
4681
4723
  <button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",