@cccarv82/freya 2.3.13 → 2.4.1

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.
Files changed (37) hide show
  1. package/.agent/rules/freya/agents/coach.mdc +7 -16
  2. package/.agent/rules/freya/agents/ingestor.mdc +1 -89
  3. package/.agent/rules/freya/agents/master.mdc +3 -0
  4. package/.agent/rules/freya/agents/oracle.mdc +7 -23
  5. package/cli/web-ui.css +860 -182
  6. package/cli/web-ui.js +547 -175
  7. package/cli/web.js +690 -536
  8. package/package.json +6 -3
  9. package/scripts/build-vector-index.js +85 -0
  10. package/scripts/export-obsidian.js +6 -16
  11. package/scripts/generate-blockers-report.js +5 -17
  12. package/scripts/generate-daily-summary.js +25 -58
  13. package/scripts/generate-executive-report.js +22 -204
  14. package/scripts/generate-sm-weekly-report.js +27 -92
  15. package/scripts/lib/DataLayer.js +92 -0
  16. package/scripts/lib/DataManager.js +198 -0
  17. package/scripts/lib/Embedder.js +59 -0
  18. package/scripts/lib/schema.js +23 -0
  19. package/scripts/migrate-v1-v2.js +184 -0
  20. package/scripts/validate-data.js +48 -51
  21. package/scripts/validate-structure.js +12 -58
  22. package/templates/base/scripts/build-vector-index.js +85 -0
  23. package/templates/base/scripts/export-obsidian.js +143 -0
  24. package/templates/base/scripts/generate-daily-summary.js +25 -58
  25. package/templates/base/scripts/generate-executive-report.js +14 -225
  26. package/templates/base/scripts/generate-sm-weekly-report.js +9 -91
  27. package/templates/base/scripts/index/build-index.js +13 -0
  28. package/templates/base/scripts/index/update-index.js +15 -0
  29. package/templates/base/scripts/lib/DataLayer.js +92 -0
  30. package/templates/base/scripts/lib/DataManager.js +198 -0
  31. package/templates/base/scripts/lib/Embedder.js +59 -0
  32. package/templates/base/scripts/lib/index-utils.js +407 -0
  33. package/templates/base/scripts/lib/schema.js +23 -0
  34. package/templates/base/scripts/lib/search-utils.js +183 -0
  35. package/templates/base/scripts/migrate-v1-v2.js +184 -0
  36. package/templates/base/scripts/validate-data.js +48 -51
  37. package/templates/base/scripts/validate-structure.js +10 -32
package/cli/web-ui.js CHANGED
@@ -183,16 +183,22 @@
183
183
  const raw = String(text || '');
184
184
  if (opts.markdown) {
185
185
  body.innerHTML = renderMarkdown(raw);
186
+ } else if (opts.html) {
187
+ body.innerHTML = raw;
186
188
  } else {
187
189
  body.innerHTML = escapeHtml(raw).replace(/\n/g, '<br>');
188
190
  }
189
191
 
192
+ if (opts.id) bubble.id = opts.id;
193
+
190
194
  bubble.appendChild(meta);
191
195
  bubble.appendChild(body);
192
196
  thread.appendChild(bubble);
193
197
 
194
- // persist
195
- persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
198
+ // persist only if it's a real chat message (not HTML typing indicator)
199
+ if (!opts.html) {
200
+ persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
201
+ }
196
202
 
197
203
  // keep newest in view
198
204
  try {
@@ -265,13 +271,60 @@
265
271
 
266
272
  chatAppend('user', query);
267
273
  setPill('run', 'pesquisando…');
274
+
275
+ // Add typing indicator
276
+ const typingId = 'typing-' + Date.now();
277
+ const typingHtml = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
278
+ chatAppend('assistant', typingHtml, { id: typingId, html: true });
279
+
268
280
  try {
269
281
  const sessionId = ensureChatSession();
270
282
  const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query });
271
283
  const answer = r && r.answer ? r.answer : 'Não encontrei registro';
284
+
285
+ const el = $(typingId);
286
+ if (el) el.remove();
287
+
272
288
  chatAppend('assistant', answer, { markdown: true });
273
289
  setPill('ok', 'pronto');
274
290
  } catch (e) {
291
+ const el = $(typingId);
292
+ if (el) el.remove();
293
+
294
+ setPill('err', 'falhou');
295
+ chatAppend('assistant', String(e && e.message ? e.message : e));
296
+ }
297
+ }
298
+
299
+ async function askFreyaInline() {
300
+ const input = $('oracleInput');
301
+ const query = input ? input.value.trim() : '';
302
+ if (!query) return;
303
+
304
+ if (input) input.value = '';
305
+
306
+ chatAppend('user', query);
307
+ setPill('run', 'pesquisando…');
308
+
309
+ // Add typing indicator
310
+ const typingId = 'typing-' + Date.now();
311
+ const typingHtml = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
312
+ chatAppend('assistant', typingHtml, { id: typingId, html: true });
313
+
314
+ try {
315
+ const sessionId = ensureChatSession();
316
+ const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query });
317
+ const answer = r && r.answer ? r.answer : 'Não encontrei registro';
318
+
319
+ const el = $(typingId);
320
+ if (el) el.remove();
321
+
322
+ chatAppend('assistant', answer, { markdown: true });
323
+ setPill('ok', 'pronto');
324
+ } catch (e) {
325
+ const el = $(typingId);
326
+ if (el) el.remove();
327
+
275
328
  setPill('err', 'falhou');
276
329
  chatAppend('assistant', String(e && e.message ? e.message : e));
277
330
  }
@@ -544,118 +597,220 @@
544
597
  }
545
598
  }
546
599
 
600
+ function setReportsTab(tab) {
601
+ state.reportsTab = tab;
602
+ const tabChrono = $('tabChrono');
603
+ const tabType = $('tabType');
604
+ if (tabChrono) tabChrono.className = tab === 'chrono' ? 'pill ok' : 'pill';
605
+ if (tabType) tabType.className = tab === 'type' ? 'pill ok' : 'pill';
606
+ renderReportsPage();
607
+ }
608
+
609
+ function togglePinReport(relPath) {
610
+ if (!state.pinnedReports) {
611
+ try { state.pinnedReports = JSON.parse(localStorage.getItem('freya_pinned_reports') || '{}'); }
612
+ catch { state.pinnedReports = {}; }
613
+ }
614
+ if (state.pinnedReports[relPath]) {
615
+ delete state.pinnedReports[relPath];
616
+ } else {
617
+ state.pinnedReports[relPath] = true;
618
+ }
619
+ try { localStorage.setItem('freya_pinned_reports', JSON.stringify(state.pinnedReports)); } catch { }
620
+ renderReportsPage();
621
+ }
622
+
547
623
  function renderReportsPage() {
548
624
  const grid = $('reportsGrid');
549
625
  if (!grid) return;
550
626
  const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
627
+
628
+ if (!state.pinnedReports) {
629
+ try { state.pinnedReports = JSON.parse(localStorage.getItem('freya_pinned_reports') || '{}'); }
630
+ catch { state.pinnedReports = {}; }
631
+ }
632
+
551
633
  const list = (state.reports || []).filter((it) => {
552
634
  if (!q) return true;
553
635
  return (it.name + ' ' + it.kind).toLowerCase().includes(q);
554
636
  });
555
637
 
556
- grid.innerHTML = '';
638
+ const groups = {};
639
+ const addGroup = (g, item) => {
640
+ if (!groups[g]) groups[g] = [];
641
+ groups[g].push(item);
642
+ };
643
+
644
+ const now = Date.now();
557
645
  for (const item of list) {
558
- const card = document.createElement('div');
559
- const mode = state.reportModes[item.relPath] || 'preview';
560
- const expanded = state.reportExpanded && state.reportExpanded[item.relPath];
561
- card.className = 'reportCard' + (mode === 'raw' ? ' raw' : '') + (expanded ? ' expanded' : '');
646
+ if (state.pinnedReports[item.relPath]) {
647
+ addGroup('📌 Fixados', item);
648
+ continue;
649
+ }
650
+ if (state.reportsTab === 'type') {
651
+ const typeName = item.kind ? String(item.kind).toUpperCase() : 'OUTROS';
652
+ addGroup(typeName, item);
653
+ } else {
654
+ const days = (now - item.mtimeMs) / (1000 * 3600 * 24);
655
+ if (days <= 1) addGroup('📅 Hoje', item);
656
+ else if (days <= 7) addGroup('📅 Esta Semana', item);
657
+ else addGroup('📅 Antigos', item);
658
+ }
659
+ }
562
660
 
563
- const meta = fmtWhen(item.mtimeMs);
564
- card.innerHTML =
565
- '<div class="reportHead" data-action="expand">'
566
- + '<div>'
567
- + '<div class="reportName">' + escapeHtml(item.name) + '</div>'
568
- + '<div class="reportMeta">'
569
- + '<span class="reportMetaText">' + escapeHtml(item.relPath) + ' • ' + escapeHtml(meta) + '</span>'
570
- + '<button class="iconBtn" data-action="copy" title="Copiar">⧉</button>'
571
- + '<button class="iconBtn" data-action="pdf" title="Baixar PDF">⬇</button>'
572
- + '</div>'
573
- + '</div>'
574
- + '<div class="reportHeadActions">'
575
- + '<button class="btn small primary" data-action="save">Salvar</button>'
576
- + '</div>'
577
- + '</div>'
578
- + '<div class="reportBody">'
579
- + '<div class="reportPreview" contenteditable="true"></div>'
580
- + '</div>';
661
+ grid.innerHTML = '';
581
662
 
582
- const text = state.reportTexts[item.relPath] || '';
583
- const preview = card.querySelector('.reportPreview');
584
- if (preview) preview.innerHTML = renderMarkdown(text || '');
585
-
586
- if (preview) {
587
- preview.addEventListener('focus', () => {
588
- preview.dataset.editing = '1';
589
- preview.textContent = state.reportTexts[item.relPath] || '';
590
- });
591
- preview.addEventListener('blur', () => {
592
- preview.dataset.editing = '';
593
- const val = preview.innerText || '';
594
- state.reportTexts[item.relPath] = val;
595
- preview.innerHTML = renderMarkdown(val);
596
- });
597
- }
663
+ let groupNames = Object.keys(groups);
664
+ if (state.reportsTab === 'type') {
665
+ groupNames.sort();
666
+ groupNames = groupNames.filter(g => g !== '📌 Fixados');
667
+ if (groups['📌 Fixados']) groupNames.unshift('📌 Fixados');
668
+ } else {
669
+ const order = ['📌 Fixados', '📅 Hoje', '📅 Esta Semana', '📅 Antigos'];
670
+ groupNames.sort((a, b) => {
671
+ let ia = order.indexOf(a);
672
+ let ib = order.indexOf(b);
673
+ if (ia === -1) ia = 99;
674
+ if (ib === -1) ib = 99;
675
+ return ia - ib;
676
+ });
677
+ }
598
678
 
599
- const saveBtn = card.querySelector('[data-action="save"]');
600
- if (saveBtn) {
601
- saveBtn.onclick = async (ev) => {
602
- ev.stopPropagation();
603
- try {
604
- const content = (raw && typeof raw.value === 'string') ? raw.value : (state.reportTexts[item.relPath] || '');
605
- setPill('run', 'salvando…');
606
- await api('/api/reports/write', { dir: dirOrDefault(), relPath: item.relPath, text: content });
607
- state.reportTexts[item.relPath] = content;
608
- setPill('ok', 'salvo');
609
- setTimeout(() => setPill('ok', 'pronto'), 800);
610
- renderReportsPage();
611
- } catch (e) {
612
- setPill('err', 'falhou');
613
- }
614
- };
615
- }
679
+ for (const g of groupNames) {
680
+ const gList = groups[g];
681
+ if (!gList || !gList.length) continue;
682
+
683
+ const head = document.createElement('div');
684
+ head.className = 'help';
685
+ head.style.fontWeight = '800';
686
+ head.style.marginTop = '16px';
687
+ head.style.marginBottom = '8px';
688
+ head.style.fontSize = '14px';
689
+ head.textContent = g;
690
+ grid.appendChild(head);
691
+
692
+ for (const item of gList) {
693
+ const card = document.createElement('div');
694
+ const mode = state.reportModes[item.relPath] || 'preview';
695
+ const expanded = state.reportExpanded && state.reportExpanded[item.relPath];
696
+ card.className = 'reportCard' + (mode === 'raw' ? ' raw' : '') + (expanded ? ' expanded' : '');
697
+
698
+ const meta = fmtWhen(item.mtimeMs);
699
+
700
+ // Determine pill style based on report kind
701
+ let kindColor = 'info';
702
+ if (item.kind === 'daily') kindColor = 'ok';
703
+ if (item.kind === 'executive') kindColor = 'warn';
704
+ if (item.kind === 'blockers') kindColor = 'err';
705
+
706
+ card.innerHTML =
707
+ '<div class="reportHead" data-action="expand">'
708
+ + '<div style="flex: 1">'
709
+ + '<div style="display: flex; align-items: center; gap: 8px;">'
710
+ + '<div class="reportName" style="font-size: 15px;">' + escapeHtml(item.name) + '</div>'
711
+ + '<span class="pill ' + kindColor + '" style="font-size: 10px; padding: 2px 6px;">' + escapeHtml(item.kind || 'report') + '</span>'
712
+ + '</div>'
713
+ + '<div class="reportMeta" style="margin-top: 6px;">'
714
+ + '<span class="reportMetaText">' + escapeHtml(item.relPath) + ' • ' + escapeHtml(meta) + '</span>'
715
+ + `<button class="iconBtn" data-action="pin" title="Fixar" style="margin-left: 8px; color:${state.pinnedReports[item.relPath] ? 'var(--warn)' : 'inherit'}">📌</button>`
716
+ + '<button class="iconBtn" data-action="copy" title="Copiar" style="margin-left: 8px;">⧉</button>'
717
+ + '<button class="iconBtn" data-action="pdf" title="Baixar PDF">⬇</button>'
718
+ + '</div>'
719
+ + '</div>'
720
+ + '<div class="reportHeadActions">'
721
+ + '<button class="btn small primary" data-action="save">Salvar</button>'
722
+ + '</div>'
723
+ + '</div>'
724
+ + '<div class="reportBody">'
725
+ + '<div class="reportPreview" contenteditable="true"></div>'
726
+ + '</div>';
727
+
728
+ const text = state.reportTexts[item.relPath] || '';
729
+ const preview = card.querySelector('.reportPreview');
730
+ if (preview) preview.innerHTML = renderMarkdown(text || '');
731
+
732
+ if (preview) {
733
+ preview.addEventListener('focus', () => {
734
+ preview.dataset.editing = '1';
735
+ preview.textContent = state.reportTexts[item.relPath] || '';
736
+ });
737
+ preview.addEventListener('blur', () => {
738
+ preview.dataset.editing = '';
739
+ const val = preview.innerText || '';
740
+ state.reportTexts[item.relPath] = val;
741
+ preview.innerHTML = renderMarkdown(val);
742
+ });
743
+ }
744
+
745
+ const saveBtn = card.querySelector('[data-action="save"]');
746
+ if (saveBtn) {
747
+ saveBtn.onclick = async (ev) => {
748
+ ev.stopPropagation();
749
+ try {
750
+ const content = (raw && typeof raw.value === 'string') ? raw.value : (state.reportTexts[item.relPath] || '');
751
+ setPill('run', 'salvando…');
752
+ await api('/api/reports/write', { dir: dirOrDefault(), relPath: item.relPath, text: content });
753
+ state.reportTexts[item.relPath] = content;
754
+ setPill('ok', 'salvo');
755
+ setTimeout(() => setPill('ok', 'pronto'), 800);
756
+ renderReportsPage();
757
+ } catch (e) {
758
+ setPill('err', 'falhou');
759
+ }
760
+ };
761
+ }
616
762
 
617
- const copyBtn = card.querySelector('[data-action="copy"]');
618
- if (copyBtn) {
619
- copyBtn.onclick = async (ev) => {
620
- ev.stopPropagation();
621
- try {
622
- const html = renderMarkdown(state.reportTexts[item.relPath] || '');
623
- const text = (preview && preview.innerText) ? preview.innerText : (state.reportTexts[item.relPath] || '');
624
- const blob = new Blob([`<div>${html}</div>`], { type: 'text/html' });
625
- const data = [new ClipboardItem({ 'text/html': blob, 'text/plain': new Blob([text], { type: 'text/plain' }) })];
626
- await navigator.clipboard.write(data);
627
- setPill('ok', 'copiado');
628
- setTimeout(() => setPill('ok', 'pronto'), 800);
629
- } catch {
763
+ const copyBtn = card.querySelector('[data-action="copy"]');
764
+ if (copyBtn) {
765
+ copyBtn.onclick = async (ev) => {
766
+ ev.stopPropagation();
630
767
  try {
631
- await navigator.clipboard.writeText(state.reportTexts[item.relPath] || '');
768
+ const html = renderMarkdown(state.reportTexts[item.relPath] || '');
769
+ const text = (preview && preview.innerText) ? preview.innerText : (state.reportTexts[item.relPath] || '');
770
+ const blob = new Blob([`<div>${html}</div>`], { type: 'text/html' });
771
+ const data = [new ClipboardItem({ 'text/html': blob, 'text/plain': new Blob([text], { type: 'text/plain' }) })];
772
+ await navigator.clipboard.write(data);
632
773
  setPill('ok', 'copiado');
633
774
  setTimeout(() => setPill('ok', 'pronto'), 800);
634
775
  } catch {
635
- setPill('err', 'copy failed');
776
+ try {
777
+ await navigator.clipboard.writeText(state.reportTexts[item.relPath] || '');
778
+ setPill('ok', 'copiado');
779
+ setTimeout(() => setPill('ok', 'pronto'), 800);
780
+ } catch {
781
+ setPill('err', 'copy failed');
782
+ }
636
783
  }
637
- }
638
- };
639
- }
784
+ };
785
+ }
640
786
 
641
- const pdfBtn = card.querySelector('[data-action="pdf"]');
642
- if (pdfBtn) {
643
- pdfBtn.onclick = (ev) => {
644
- ev.stopPropagation();
645
- downloadReportPdf(item);
646
- };
647
- }
787
+ const pdfBtn = card.querySelector('[data-action="pdf"]');
788
+ if (pdfBtn) {
789
+ pdfBtn.onclick = (ev) => {
790
+ ev.stopPropagation();
791
+ downloadReportPdf(item);
792
+ };
793
+ }
648
794
 
649
- const head = card.querySelector('[data-action="expand"]');
650
- if (head) {
651
- head.onclick = (ev) => {
652
- if (ev.target && ev.target.closest('.reportHeadActions')) return;
653
- state.reportExpanded[item.relPath] = !state.reportExpanded[item.relPath];
654
- renderReportsPage();
655
- };
656
- }
795
+ const head = card.querySelector('[data-action="expand"]');
796
+ if (head) {
797
+ head.onclick = (ev) => {
798
+ if (ev.target && ev.target.closest('.reportHeadActions')) return;
799
+ state.reportExpanded[item.relPath] = !state.reportExpanded[item.relPath];
800
+ renderReportsPage();
801
+ };
802
+ }
657
803
 
658
- grid.appendChild(card);
804
+ const pinBtn = card.querySelector('[data-action="pin"]');
805
+ if (pinBtn) {
806
+ pinBtn.onclick = (ev) => {
807
+ ev.stopPropagation();
808
+ togglePinReport(item.relPath);
809
+ };
810
+ }
811
+
812
+ grid.appendChild(card);
813
+ }
659
814
  }
660
815
  }
661
816
 
@@ -664,22 +819,84 @@
664
819
  if (!el) return;
665
820
  const filter = String(($('projectsFilter') && $('projectsFilter').value) || '').toLowerCase();
666
821
  const items = Array.isArray(state.projects) ? state.projects : [];
822
+
823
+ // Compute severity for all projects before filtering/sorting
824
+ const now = Date.now();
825
+ for (const p of items) {
826
+ let blockers = 0;
827
+ let highTasks = 0;
828
+ if (state.allBlockers) blockers = state.allBlockers.filter(b => b.projectSlug === p.slug).length;
829
+ if (state.allTasks) highTasks = state.allTasks.filter(t => t.projectSlug === p.slug && (t.priority || '').toUpperCase() === 'HIGH').length;
830
+
831
+ const daysSinceUpdate = p.lastUpdated ? (now - new Date(p.lastUpdated).getTime()) / (1000 * 3600 * 24) : Infinity;
832
+
833
+ let traffic = '🟢';
834
+ let severity = 3;
835
+ if (blockers > 0 || !p.active) {
836
+ traffic = '🔴';
837
+ severity = 1;
838
+ } else if (daysSinceUpdate > 14 || highTasks > 0) {
839
+ traffic = '🟡';
840
+ severity = 2;
841
+ }
842
+
843
+ p._severity = severity;
844
+ p._traffic = traffic;
845
+ p._blockersCount = blockers;
846
+ p._highTasks = highTasks;
847
+ }
848
+
667
849
  const filtered = items.filter((p) => {
668
850
  const hay = [p.client, p.program, p.stream, p.project, p.slug, (p.tags || []).join(' ')].join(' ').toLowerCase();
669
851
  return !filter || hay.includes(filter);
670
852
  });
853
+
854
+ // Sort by severity (1=Red, 2=Yellow, 3=Green), then by lastUpdated
855
+ filtered.sort((a, b) => {
856
+ if (a._severity !== b._severity) return a._severity - b._severity;
857
+ return String(b.lastUpdated || '').localeCompare(String(a.lastUpdated || ''));
858
+ });
859
+
671
860
  el.innerHTML = '';
672
861
  for (const p of filtered) {
673
862
  const card = document.createElement('div');
674
863
  card.className = 'reportCard';
864
+
865
+ let trafficLabel = '';
866
+ if (p._traffic === '🔴') trafficLabel = p._blockersCount > 0 ? `${p._blockersCount} Blockers` : 'Inativo';
867
+ else if (p._traffic === '🟡') trafficLabel = p._highTasks > 0 ? `${p._highTasks} High Tasks` : 'Desatualizado';
868
+ else trafficLabel = 'Saudável';
869
+
870
+ const renderActivityBars = (count) => {
871
+ const c = Number(count) || 0;
872
+ let level = 0;
873
+ if (c > 0) level = 1;
874
+ if (c > 3) level = 2;
875
+ if (c > 8) level = 3;
876
+ if (c > 15) level = 4;
877
+ if (c > 25) level = 5;
878
+ let html = '<div style="display:flex; gap:2px; height:12px; align-items:flex-end;" title="' + c + ' eventos históricos">';
879
+ for (let i = 1; i <= 5; i++) {
880
+ const h = 4 + (i * 1.5);
881
+ const color = i <= level ? 'var(--accent)' : 'var(--border)';
882
+ html += `<div style="width:4px; height:${h}px; background:${color}; border-radius:2px;"></div>`;
883
+ }
884
+ html += '</div>';
885
+ return html;
886
+ };
887
+
675
888
  card.innerHTML = '<div class="reportHead">'
676
- + '<div><div class="reportTitle">' + escapeHtml(p.project || p.slug || 'Projeto') + '</div>'
677
- + '<div class="reportMeta">' + escapeHtml([p.client, p.program, p.stream].filter(Boolean).join(' · ')) + '</div></div>'
889
+ + '<div><div class="reportTitle" style="display:flex; align-items:center; gap:8px;">'
890
+ + '<span title="' + trafficLabel + '" style="font-size:16px; cursor:help;">' + p._traffic + '</span>'
891
+ + escapeHtml(p.project || p.slug || 'Projeto') + '</div>'
892
+ + '<div class="reportMeta" style="margin-top:4px;">' + escapeHtml([p.client, p.program, p.stream].filter(Boolean).join(' · ')) + '</div></div>'
678
893
  + '<div class="reportActions">' + (p.active ? '<span class="pill ok">ativo</span>' : '<span class="pill warn">inativo</span>') + '</div>'
679
894
  + '</div>'
680
- + '<div class="help" style="margin-top:8px">' + escapeHtml(p.currentStatus || 'Sem status') + '</div>'
681
- + '<div class="reportMeta" style="margin-top:8px">Última atualização: ' + escapeHtml(p.lastUpdated || '—') + '</div>'
682
- + '<div class="reportMeta">Eventos: ' + escapeHtml(String(p.historyCount || 0)) + '</div>';
895
+ + '<div class="help" style="margin-top:12px; font-weight:500;">' + escapeHtml(p.currentStatus || 'Sem status') + '</div>'
896
+ + '<div style="margin-top:12px; display:flex; justify-content:space-between; align-items:center; font-size:11px; opacity:0.8;">'
897
+ + '<div>Última att: <b>' + escapeHtml(p.lastUpdated || '—') + '</b></div>'
898
+ + '<div style="display:flex; align-items:center; gap:6px;">Atividade: ' + renderActivityBars(p.historyCount) + '</div>'
899
+ + '</div>';
683
900
  el.appendChild(card);
684
901
  }
685
902
  if (!filtered.length) {
@@ -692,8 +909,14 @@
692
909
 
693
910
  async function refreshProjects() {
694
911
  try {
695
- const r = await api('/api/projects/list', { dir: dirOrDefault() });
912
+ const [r, b, t] = await Promise.all([
913
+ api('/api/projects/list', { dir: dirOrDefault() }),
914
+ api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 1000 }),
915
+ api('/api/tasks/list', { dir: dirOrDefault(), status: 'PENDING', limit: 1000 })
916
+ ]);
696
917
  state.projects = r.projects || [];
918
+ state.allBlockers = b.blockers || [];
919
+ state.allTasks = t.tasks || [];
697
920
  renderProjects();
698
921
  } catch (e) {
699
922
  const el = $('projectsGrid');
@@ -778,15 +1001,50 @@
778
1001
  }
779
1002
  const card = document.createElement('div');
780
1003
  card.className = 'reportCard';
1004
+ card.style.cursor = 'pointer'; // Make it look clickable
1005
+
1006
+ let icon = '⏺';
1007
+ let colorClass = 'info';
1008
+ if (it.kind === 'task') { icon = '📋'; colorClass = 'ok'; }
1009
+ else if (it.kind === 'status') { icon = '🔄'; colorClass = 'warn'; }
1010
+ else if (it.kind === 'daily') { icon = '📝'; colorClass = 'info'; }
1011
+
781
1012
  card.innerHTML = '<div class="reportHead">'
782
- + '<div><div class="reportTitle">' + escapeHtml(it.title || 'Evento') + '</div>'
783
- + '<div class="reportMeta">' + escapeHtml(it.date || '') + ' · ' + escapeHtml(it.kind || '') + '</div></div>'
1013
+ + '<div><div class="reportTitle" style="display:flex; align-items:center; gap:8px;">'
1014
+ + '<span style="font-size:16px;">' + icon + '</span>'
1015
+ + escapeHtml(it.title || 'Evento') + '</div>'
1016
+ + '<div class="reportMeta" style="margin-top:4px;">' + escapeHtml(it.date || '') + ' · <span style="text-transform:uppercase;">' + escapeHtml(it.kind || '') + '</span></div></div>'
784
1017
  + '<div class="reportActions">'
785
- + '<span class="pill info">' + escapeHtml(it.kind || '') + '</span>'
1018
+ + '<span class="pill ' + colorClass + '">' + escapeHtml(it.kind || '') + '</span>'
786
1019
  + (it.slug ? ('<span class="pill">' + escapeHtml(it.slug) + '</span>') : '')
787
1020
  + '</div>'
788
1021
  + '</div>'
789
- + '<div class="help" style="margin-top:8px">' + escapeHtml(it.content || '') + '</div>';
1022
+ + '<div class="help" style="margin-top:12px; font-weight:500;">' + escapeHtml(it.content || '') + '</div>';
1023
+
1024
+ // Interactivity
1025
+ card.onclick = () => {
1026
+ if (it.kind === 'daily') {
1027
+ // Open daily log in reports
1028
+ if (window.refreshReports) {
1029
+ const dateStr = it.date;
1030
+ const relPath = 'logs/daily/' + dateStr + '.md';
1031
+ state.selectedReport = { relPath, name: dateStr + '.md', kind: 'daily' };
1032
+ const railR = $('railReports');
1033
+ if (railR) railR.click();
1034
+ setTimeout(() => {
1035
+ if (window.refreshReportsPage) refreshReportsPage();
1036
+ if (window.renderReportsList) renderReportsList();
1037
+ }, 50);
1038
+ }
1039
+ } else {
1040
+ // Filter timeline by project
1041
+ const slugToFilter = it.slug || (it.kind === 'task' ? it.content : '');
1042
+ if (slugToFilter) {
1043
+ setTimelineProject(slugToFilter);
1044
+ }
1045
+ }
1046
+ };
1047
+
790
1048
  el.appendChild(card);
791
1049
  }
792
1050
  if (!filtered.length) {
@@ -973,45 +1231,44 @@
973
1231
  const proj = $('railProjects');
974
1232
  const tl = $('railTimeline');
975
1233
  const health = $('railCompanion');
1234
+ const graph = $('railGraph');
1235
+
1236
+ const curPage = (document.body && document.body.dataset) ? document.body.dataset.page : null;
1237
+ const isDashboard = !curPage || curPage === 'dashboard';
1238
+
976
1239
  if (dash) {
977
1240
  dash.onclick = () => {
978
- const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
979
- if (isReports) {
1241
+ if (!isDashboard) {
980
1242
  window.location.href = '/';
981
- return;
1243
+ } else {
1244
+ const c = document.querySelector('.centerBody');
1245
+ if (c) c.scrollTo({ top: 0, behavior: 'smooth' });
982
1246
  }
983
- const c = document.querySelector('.centerBody');
984
- if (c) c.scrollTo({ top: 0, behavior: 'smooth' });
985
1247
  };
986
1248
  }
987
1249
  if (rep) {
988
1250
  rep.onclick = () => {
989
- const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
990
- if (!isReports) window.location.href = '/reports';
1251
+ if (curPage !== 'reports') window.location.href = '/reports';
991
1252
  };
992
1253
  }
993
1254
  if (proj) {
994
1255
  proj.onclick = () => {
995
- const isProjects = document.body && document.body.dataset && document.body.dataset.page === 'projects';
996
- if (!isProjects) window.location.href = '/projects';
1256
+ if (curPage !== 'projects') window.location.href = '/projects';
997
1257
  };
998
1258
  }
999
- if (tl) {
1000
- tl.onclick = () => {
1001
- const isTimeline = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
1002
- if (!isTimeline) window.location.href = '/timeline';
1259
+ if (health) {
1260
+ health.onclick = () => {
1261
+ if (curPage !== 'companion') window.location.href = '/companion';
1003
1262
  };
1004
1263
  }
1005
- if ($('railGraph')) {
1006
- $('railGraph').onclick = () => {
1007
- const isGraph = document.body && document.body.dataset && document.body.dataset.page === 'graph';
1008
- if (!isGraph) window.location.href = '/graph';
1264
+ if (tl) {
1265
+ tl.onclick = () => {
1266
+ if (curPage !== 'timeline') window.location.href = '/timeline';
1009
1267
  };
1010
1268
  }
1011
- if (health) {
1012
- health.onclick = () => {
1013
- const isHealth = document.body && document.body.dataset && document.body.dataset.page === 'companion';
1014
- if (!isHealth) window.location.href = '/companion';
1269
+ if (graph) {
1270
+ graph.onclick = () => {
1271
+ if (curPage !== 'graph') window.location.href = '/graph';
1015
1272
  };
1016
1273
  }
1017
1274
  }
@@ -1054,28 +1311,52 @@
1054
1311
  }
1055
1312
  }
1056
1313
 
1057
- function renderTasks(list) {
1058
- const el = $('tasksList');
1314
+ function renderSwimlanes(tasks, blockers) {
1315
+ const el = $('swimlaneContainer');
1059
1316
  if (!el) return;
1060
1317
  el.innerHTML = '';
1061
- for (const t of list || []) {
1318
+
1319
+ const groups = {};
1320
+ const addGroup = (slug) => {
1321
+ const key = slug || 'Global / Sem Projeto';
1322
+ if (!groups[key]) groups[key] = { tasks: [], blockers: [] };
1323
+ return key;
1324
+ };
1325
+
1326
+ for (const t of tasks) groups[addGroup(t.projectSlug)].tasks.push(t);
1327
+ for (const b of blockers) groups[addGroup(b.projectSlug)].blockers.push(b);
1328
+
1329
+ const sortedKeys = Object.keys(groups).sort((a, b) => {
1330
+ if (a === 'Global / Sem Projeto') return 1;
1331
+ if (b === 'Global / Sem Projeto') return -1;
1332
+ return a.localeCompare(b);
1333
+ });
1334
+
1335
+ sortedKeys.sort((a, b) => groups[b].blockers.length - groups[a].blockers.length);
1336
+
1337
+ if (sortedKeys.length === 0) {
1338
+ const empty = document.createElement('div');
1339
+ empty.className = 'help';
1340
+ empty.textContent = 'Nenhuma tarefa ou bloqueio pendente hoje.';
1341
+ el.appendChild(empty);
1342
+ return;
1343
+ }
1344
+
1345
+ const createTaskRow = (t) => {
1062
1346
  const row = document.createElement('div');
1063
1347
  row.className = 'rep';
1064
1348
  const pri = (t.priority || '').toUpperCase();
1065
1349
  row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
1066
- + '<div style="min-width:0"><div style="font-weight:700">' + escapeHtml(t.description || '') + '</div>'
1067
- + '<div style="opacity:.7; font-size:11px; margin-top:4px">' + escapeHtml(String(t.category || ''))
1068
- + (t.projectSlug ? (' · <span style="font-family:var(--mono); opacity:.9">[' + escapeHtml(String(t.projectSlug)) + ']</span>') : '')
1350
+ + '<div style="min-width:0"><div style="font-weight:600">' + escapeHtml(t.description || '') + '</div>'
1351
+ + '<div style="opacity:.7; font-size:11px; margin-top:4px; font-weight:500;">' + escapeHtml(String(t.category || ''))
1069
1352
  + (pri ? (' · ' + escapeHtml(pri)) : '') + '</div></div>'
1070
1353
  + '<div style="display:flex; gap:8px">'
1071
- + '<button class="btn small" type="button">Concluir</button>'
1072
- + '<button class="btn small" type="button">Editar</button>'
1354
+ + '<button class="btn small complete-btn" type="button">Concluir</button>'
1355
+ + '<button class="btn small edit-btn" type="button">Editar</button>'
1073
1356
  + '</div>'
1074
1357
  + '</div>';
1075
- const btns = row.querySelectorAll('button');
1076
- const btn = btns[0];
1077
- if (btns[1]) btns[1].onclick = () => editTask(t);
1078
- btn.onclick = async () => {
1358
+ row.querySelector('.edit-btn').onclick = () => editTask(t);
1359
+ row.querySelector('.complete-btn').onclick = async () => {
1079
1360
  try {
1080
1361
  setPill('run', 'completing…');
1081
1362
  await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
@@ -1087,55 +1368,108 @@
1087
1368
  setOut(String(e && e.message ? e.message : e));
1088
1369
  }
1089
1370
  };
1090
- el.appendChild(row);
1091
- }
1092
- if (!el.childElementCount) {
1093
- const empty = document.createElement('div');
1094
- empty.className = 'help';
1095
- empty.textContent = 'Nenhuma tarefa em Fazer agora.';
1096
- el.appendChild(empty);
1097
- }
1098
- }
1371
+ return row;
1372
+ };
1099
1373
 
1100
- function renderBlockers(list) {
1101
- const el = $('blockersList');
1102
- if (!el) return;
1103
- el.innerHTML = '';
1104
- for (const b of list || []) {
1374
+ const createBlockerRow = (b) => {
1105
1375
  const row = document.createElement('div');
1106
1376
  row.className = 'rep';
1377
+ row.style.borderLeft = '4px solid var(--warn)';
1378
+ row.style.background = 'var(--warn-bg)';
1107
1379
  const sev = String(b.severity || '').toUpperCase();
1108
1380
  row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
1109
- + '<div style="min-width:0"><div style="font-weight:800">' + escapeHtml(sev) + '</div>'
1110
- + '<div style="margin-top:4px">' + escapeHtml(b.title || '')
1111
- + (b.projectSlug ? (' <span style="font-family:var(--mono); opacity:.8">[' + escapeHtml(String(b.projectSlug)) + ']</span>') : '')
1112
- + '</div>'
1381
+ + '<div style="min-width:0"><div style="font-weight:800; color:var(--warn)">' + escapeHtml(sev) + '</div>'
1382
+ + '<div style="margin-top:4px; font-weight:600;">' + escapeHtml(b.title || '') + '</div>'
1113
1383
  + '</div>'
1114
1384
  + '<div style="display:flex; gap:8px; align-items:center">'
1115
- + '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(fmtWhen(new Date(b.createdAt || Date.now()).getTime())) + '</div>'
1116
- + '<button class="btn small" type="button">Editar</button>'
1385
+ + '<div style="opacity:.7; font-size:11px; white-space:nowrap; font-weight:500;">' + escapeHtml(fmtWhen(new Date(b.createdAt || Date.now()).getTime())) + '</div>'
1386
+ + '<button class="btn small edit-btn" type="button" style="border-color:var(--warn); color:var(--warn);">Editar</button>'
1117
1387
  + '</div>'
1118
1388
  + '</div>';
1119
- const ebtn = row.querySelector('button');
1120
- if (ebtn) ebtn.onclick = () => editBlocker(b);
1121
- el.appendChild(row);
1122
- }
1123
- if (!el.childElementCount) {
1124
- const empty = document.createElement('div');
1125
- empty.className = 'help';
1126
- empty.textContent = 'Nenhum bloqueio aberto.';
1127
- el.appendChild(empty);
1389
+ row.querySelector('.edit-btn').onclick = () => editBlocker(b);
1390
+ return row;
1391
+ };
1392
+
1393
+ for (const key of sortedKeys) {
1394
+ const g = groups[key];
1395
+ const swimlane = document.createElement('div');
1396
+ swimlane.className = 'panel';
1397
+ swimlane.style.border = '1px solid var(--border)';
1398
+ swimlane.style.borderRadius = '8px';
1399
+ swimlane.style.boxShadow = 'none';
1400
+ swimlane.style.overflow = 'hidden';
1401
+
1402
+ const head = document.createElement('div');
1403
+ head.style.display = 'flex';
1404
+ head.style.justifyContent = 'space-between';
1405
+ head.style.alignItems = 'center';
1406
+ head.style.cursor = 'pointer';
1407
+ head.style.padding = '10px 16px';
1408
+ head.style.background = 'var(--bg2)';
1409
+ head.style.borderBottom = '1px solid var(--border)';
1410
+ head.style.transition = 'background 0.2s';
1411
+ head.onmouseover = () => head.style.background = 'var(--paper2)';
1412
+ head.onmouseout = () => head.style.background = 'var(--bg2)';
1413
+
1414
+ const bCount = g.blockers.length;
1415
+ const tCount = g.tasks.length;
1416
+
1417
+ head.innerHTML = `
1418
+ <div style="font-weight:700; font-size:13px; display:flex; gap:8px; align-items:center;">
1419
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.6; transition: transform 0.2s;"><polyline points="6 9 12 15 18 9"></polyline></svg>
1420
+ <span style="font-family: var(--mono); color: var(--accent);">${escapeHtml(key)}</span>
1421
+ </div>
1422
+ <div style="display:flex; gap:6px; font-size:11px;">
1423
+ ${bCount > 0 ? `<span style="color:var(--warn); background:var(--warn-bg); padding:2px 8px; border-radius:12px; font-weight:700;">🔴 ${bCount} Blockers</span>` : ''}
1424
+ ${tCount > 0 ? `<span style="color:var(--info); background:var(--bg2); border: 1px solid var(--border); padding:2px 8px; border-radius:12px; font-weight:700;">🟢 ${tCount} Tasks</span>` : ''}
1425
+ </div>
1426
+ `;
1427
+
1428
+ const body = document.createElement('div');
1429
+ body.style.display = 'flex';
1430
+ body.style.flexDirection = 'column';
1431
+ body.style.background = 'var(--bg)';
1432
+
1433
+ for (const b of g.blockers) {
1434
+ const r = createBlockerRow(b);
1435
+ r.style.margin = '0';
1436
+ r.style.borderRadius = '0';
1437
+ r.style.borderBottom = '1px solid var(--border)';
1438
+ r.style.borderLeft = '4px solid var(--warn)';
1439
+ r.style.boxShadow = 'none';
1440
+ body.appendChild(r);
1441
+ }
1442
+ for (const t of g.tasks) {
1443
+ const r = createTaskRow(t);
1444
+ r.style.margin = '0';
1445
+ r.style.borderRadius = '0';
1446
+ r.style.borderBottom = '1px solid var(--border)';
1447
+ r.style.borderLeft = '4px solid transparent';
1448
+ r.style.boxShadow = 'none';
1449
+ body.appendChild(r);
1450
+ }
1451
+
1452
+ let isOpen = true;
1453
+ head.onclick = () => {
1454
+ isOpen = !isOpen;
1455
+ body.style.display = isOpen ? 'flex' : 'none';
1456
+ const svg = head.querySelector('svg');
1457
+ if (svg) svg.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
1458
+ };
1459
+
1460
+ swimlane.appendChild(head);
1461
+ swimlane.appendChild(body);
1462
+ el.appendChild(swimlane);
1128
1463
  }
1129
1464
  }
1130
1465
 
1131
1466
  async function refreshToday() {
1132
1467
  try {
1133
1468
  const [t, b] = await Promise.all([
1134
- api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 5 }),
1135
- api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 5 })
1469
+ api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 50 }),
1470
+ api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 50 })
1136
1471
  ]);
1137
- renderTasks((t && t.tasks) || []);
1138
- renderBlockers((b && b.blockers) || []);
1472
+ renderSwimlanes((t && t.tasks) || [], (b && b.blockers) || []);
1139
1473
  refreshBlockersInsights();
1140
1474
  } catch (e) {
1141
1475
  // keep silent in background refresh
@@ -1355,25 +1689,34 @@
1355
1689
  const nodes = new vis.DataSet(r.nodes);
1356
1690
  const edges = new vis.DataSet(r.edges);
1357
1691
 
1692
+ const style = getComputedStyle(document.body);
1693
+ const bg = style.getPropertyValue('--bg').trim() || '#0f1115';
1694
+ const paper = style.getPropertyValue('--paper').trim() || '#14161a';
1695
+ const primary = style.getPropertyValue('--primary').trim() || '#38bdf8';
1696
+ const accent = style.getPropertyValue('--accent').trim() || '#2dd4bf';
1697
+ const text = style.getPropertyValue('--text').trim() || '#f1f5f9';
1698
+ const line2 = style.getPropertyValue('--line2').trim() || 'rgba(255,255,255,0.12)';
1699
+ const sansFont = style.getPropertyValue('--sans').trim() || 'Inter, sans-serif';
1700
+
1358
1701
  const data = { nodes, edges };
1359
1702
  const options = {
1360
1703
  nodes: {
1361
1704
  shape: 'dot',
1362
1705
  size: 16,
1363
- font: { color: 'var(--fg)', face: 'var(--sans)' },
1706
+ font: { color: text, face: sansFont },
1364
1707
  borderWidth: 2
1365
1708
  },
1366
1709
  edges: {
1367
- color: { color: 'var(--border)', highlight: 'var(--brand)' },
1710
+ color: { color: line2, highlight: primary },
1368
1711
  width: 1,
1369
1712
  smooth: { type: 'continuous' }
1370
1713
  },
1371
1714
  groups: {
1372
- project: { color: { background: 'var(--bg2)', border: 'var(--brand)' } },
1373
- task: { color: { background: 'var(--bg2)', border: 'var(--info)' }, size: 10 },
1374
- blocker: { color: { background: 'var(--warn-bg)', border: 'var(--warn)' }, size: 14, shape: 'triangle' },
1375
- tag: { color: { background: 'var(--bg2)', border: 'var(--border)' }, size: 8, font: { size: 10 } },
1376
- unassigned: { color: { background: 'var(--bg2)', border: 'var(--border)' }, size: 10 }
1715
+ project: { color: { background: paper, border: primary } },
1716
+ task: { color: { background: paper, border: accent }, size: 10 },
1717
+ blocker: { color: { background: 'rgba(239, 68, 68, 0.15)', border: '#ef4444' }, size: 14, shape: 'triangle' },
1718
+ tag: { color: { background: bg, border: line2 }, size: 8, font: { size: 10, color: text } },
1719
+ unassigned: { color: { background: bg, border: line2 }, size: 10 }
1377
1720
  },
1378
1721
  physics: {
1379
1722
  forceAtlas2Based: { gravitationalConstant: -26, centralGravity: 0.005, springLength: 230, springConstant: 0.18 },
@@ -1388,6 +1731,32 @@
1388
1731
  state.networkInstance.destroy();
1389
1732
  }
1390
1733
  state.networkInstance = new vis.Network(el, data, options);
1734
+
1735
+ // Make nodes actionable
1736
+ state.networkInstance.on('click', (params) => {
1737
+ if (params.nodes.length > 0) {
1738
+ const nodeId = String(params.nodes[0]);
1739
+ if (nodeId.startsWith('tag:')) {
1740
+ const tag = nodeId.replace('tag:', '');
1741
+ const railP = $('railProjects');
1742
+ const pf = $('projectsFilter');
1743
+ if (railP && pf) {
1744
+ pf.value = tag;
1745
+ railP.click();
1746
+ setTimeout(() => { if (window.renderProjects) renderProjects(); }, 50);
1747
+ }
1748
+ } else if (nodeId.startsWith('task:') || nodeId.startsWith('blocker:')) {
1749
+ const railD = $('railDashboard');
1750
+ if (railD) railD.click();
1751
+ } else if (nodeId !== 'unassigned_tasks' && nodeId !== 'unassigned_blockers') {
1752
+ // It's a project slug
1753
+ setTimelineProject(nodeId);
1754
+ const railT = $('railTimeline');
1755
+ if (railT) railT.click();
1756
+ }
1757
+ }
1758
+ });
1759
+
1391
1760
  } catch (e) {
1392
1761
  console.error('Failed to load graph data', e);
1393
1762
  }
@@ -1884,6 +2253,8 @@
1884
2253
  window.renderReportsList = renderReportsList;
1885
2254
  window.renderReportsPage = renderReportsPage;
1886
2255
  window.refreshReportsPage = refreshReportsPage;
2256
+ window.setReportsTab = setReportsTab;
2257
+ window.togglePinReport = togglePinReport;
1887
2258
  window.refreshProjects = refreshProjects;
1888
2259
  window.refreshTimeline = refreshTimeline;
1889
2260
  window.refreshGraph = refreshGraph;
@@ -1911,4 +2282,5 @@
1911
2282
  window.runSuggestedReports = runSuggestedReports;
1912
2283
  window.exportChatObsidian = exportChatObsidian;
1913
2284
  window.askFreya = askFreya;
2285
+ window.askFreyaInline = askFreyaInline;
1914
2286
  })();