@cccarv82/freya 2.3.13 → 2.5.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.
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 +965 -182
  6. package/cli/web-ui.js +551 -173
  7. package/cli/web.js +863 -536
  8. package/package.json +7 -4
  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,50 @@
973
1231
  const proj = $('railProjects');
974
1232
  const tl = $('railTimeline');
975
1233
  const health = $('railCompanion');
1234
+ const graph = $('railGraph');
1235
+ const docs = $('railDocs');
1236
+
1237
+ const curPage = (document.body && document.body.dataset) ? document.body.dataset.page : null;
1238
+ const isDashboard = !curPage || curPage === 'dashboard';
1239
+
976
1240
  if (dash) {
977
1241
  dash.onclick = () => {
978
- const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
979
- if (isReports) {
1242
+ if (!isDashboard) {
980
1243
  window.location.href = '/';
981
- return;
1244
+ } else {
1245
+ const c = document.querySelector('.centerBody');
1246
+ if (c) c.scrollTo({ top: 0, behavior: 'smooth' });
982
1247
  }
983
- const c = document.querySelector('.centerBody');
984
- if (c) c.scrollTo({ top: 0, behavior: 'smooth' });
985
1248
  };
986
1249
  }
987
1250
  if (rep) {
988
1251
  rep.onclick = () => {
989
- const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
990
- if (!isReports) window.location.href = '/reports';
1252
+ if (curPage !== 'reports') window.location.href = '/reports';
991
1253
  };
992
1254
  }
993
1255
  if (proj) {
994
1256
  proj.onclick = () => {
995
- const isProjects = document.body && document.body.dataset && document.body.dataset.page === 'projects';
996
- if (!isProjects) window.location.href = '/projects';
1257
+ if (curPage !== 'projects') window.location.href = '/projects';
1258
+ };
1259
+ }
1260
+ if (health) {
1261
+ health.onclick = () => {
1262
+ if (curPage !== 'companion') window.location.href = '/companion';
997
1263
  };
998
1264
  }
999
1265
  if (tl) {
1000
1266
  tl.onclick = () => {
1001
- const isTimeline = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
1002
- if (!isTimeline) window.location.href = '/timeline';
1267
+ if (curPage !== 'timeline') window.location.href = '/timeline';
1003
1268
  };
1004
1269
  }
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';
1270
+ if (graph) {
1271
+ graph.onclick = () => {
1272
+ if (curPage !== 'graph') window.location.href = '/graph';
1009
1273
  };
1010
1274
  }
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';
1275
+ if (docs) {
1276
+ docs.onclick = () => {
1277
+ if (curPage !== 'docs') window.location.href = '/docs';
1015
1278
  };
1016
1279
  }
1017
1280
  }
@@ -1054,28 +1317,52 @@
1054
1317
  }
1055
1318
  }
1056
1319
 
1057
- function renderTasks(list) {
1058
- const el = $('tasksList');
1320
+ function renderSwimlanes(tasks, blockers) {
1321
+ const el = $('swimlaneContainer');
1059
1322
  if (!el) return;
1060
1323
  el.innerHTML = '';
1061
- for (const t of list || []) {
1324
+
1325
+ const groups = {};
1326
+ const addGroup = (slug) => {
1327
+ const key = slug || 'Global / Sem Projeto';
1328
+ if (!groups[key]) groups[key] = { tasks: [], blockers: [] };
1329
+ return key;
1330
+ };
1331
+
1332
+ for (const t of tasks) groups[addGroup(t.projectSlug)].tasks.push(t);
1333
+ for (const b of blockers) groups[addGroup(b.projectSlug)].blockers.push(b);
1334
+
1335
+ const sortedKeys = Object.keys(groups).sort((a, b) => {
1336
+ if (a === 'Global / Sem Projeto') return 1;
1337
+ if (b === 'Global / Sem Projeto') return -1;
1338
+ return a.localeCompare(b);
1339
+ });
1340
+
1341
+ sortedKeys.sort((a, b) => groups[b].blockers.length - groups[a].blockers.length);
1342
+
1343
+ if (sortedKeys.length === 0) {
1344
+ const empty = document.createElement('div');
1345
+ empty.className = 'help';
1346
+ empty.textContent = 'Nenhuma tarefa ou bloqueio pendente hoje.';
1347
+ el.appendChild(empty);
1348
+ return;
1349
+ }
1350
+
1351
+ const createTaskRow = (t) => {
1062
1352
  const row = document.createElement('div');
1063
1353
  row.className = 'rep';
1064
1354
  const pri = (t.priority || '').toUpperCase();
1065
1355
  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>') : '')
1356
+ + '<div style="min-width:0"><div style="font-weight:600">' + escapeHtml(t.description || '') + '</div>'
1357
+ + '<div style="opacity:.7; font-size:11px; margin-top:4px; font-weight:500;">' + escapeHtml(String(t.category || ''))
1069
1358
  + (pri ? (' · ' + escapeHtml(pri)) : '') + '</div></div>'
1070
1359
  + '<div style="display:flex; gap:8px">'
1071
- + '<button class="btn small" type="button">Concluir</button>'
1072
- + '<button class="btn small" type="button">Editar</button>'
1360
+ + '<button class="btn small complete-btn" type="button">Concluir</button>'
1361
+ + '<button class="btn small edit-btn" type="button">Editar</button>'
1073
1362
  + '</div>'
1074
1363
  + '</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 () => {
1364
+ row.querySelector('.edit-btn').onclick = () => editTask(t);
1365
+ row.querySelector('.complete-btn').onclick = async () => {
1079
1366
  try {
1080
1367
  setPill('run', 'completing…');
1081
1368
  await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
@@ -1087,55 +1374,108 @@
1087
1374
  setOut(String(e && e.message ? e.message : e));
1088
1375
  }
1089
1376
  };
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
- }
1377
+ return row;
1378
+ };
1099
1379
 
1100
- function renderBlockers(list) {
1101
- const el = $('blockersList');
1102
- if (!el) return;
1103
- el.innerHTML = '';
1104
- for (const b of list || []) {
1380
+ const createBlockerRow = (b) => {
1105
1381
  const row = document.createElement('div');
1106
1382
  row.className = 'rep';
1383
+ row.style.borderLeft = '4px solid var(--warn)';
1384
+ row.style.background = 'var(--warn-bg)';
1107
1385
  const sev = String(b.severity || '').toUpperCase();
1108
1386
  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>'
1387
+ + '<div style="min-width:0"><div style="font-weight:800; color:var(--warn)">' + escapeHtml(sev) + '</div>'
1388
+ + '<div style="margin-top:4px; font-weight:600;">' + escapeHtml(b.title || '') + '</div>'
1113
1389
  + '</div>'
1114
1390
  + '<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>'
1391
+ + '<div style="opacity:.7; font-size:11px; white-space:nowrap; font-weight:500;">' + escapeHtml(fmtWhen(new Date(b.createdAt || Date.now()).getTime())) + '</div>'
1392
+ + '<button class="btn small edit-btn" type="button" style="border-color:var(--warn); color:var(--warn);">Editar</button>'
1117
1393
  + '</div>'
1118
1394
  + '</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);
1395
+ row.querySelector('.edit-btn').onclick = () => editBlocker(b);
1396
+ return row;
1397
+ };
1398
+
1399
+ for (const key of sortedKeys) {
1400
+ const g = groups[key];
1401
+ const swimlane = document.createElement('div');
1402
+ swimlane.className = 'panel';
1403
+ swimlane.style.border = '1px solid var(--border)';
1404
+ swimlane.style.borderRadius = '8px';
1405
+ swimlane.style.boxShadow = 'none';
1406
+ swimlane.style.overflow = 'hidden';
1407
+
1408
+ const head = document.createElement('div');
1409
+ head.style.display = 'flex';
1410
+ head.style.justifyContent = 'space-between';
1411
+ head.style.alignItems = 'center';
1412
+ head.style.cursor = 'pointer';
1413
+ head.style.padding = '10px 16px';
1414
+ head.style.background = 'var(--bg2)';
1415
+ head.style.borderBottom = '1px solid var(--border)';
1416
+ head.style.transition = 'background 0.2s';
1417
+ head.onmouseover = () => head.style.background = 'var(--paper2)';
1418
+ head.onmouseout = () => head.style.background = 'var(--bg2)';
1419
+
1420
+ const bCount = g.blockers.length;
1421
+ const tCount = g.tasks.length;
1422
+
1423
+ head.innerHTML = `
1424
+ <div style="font-weight:700; font-size:13px; display:flex; gap:8px; align-items:center;">
1425
+ <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>
1426
+ <span style="font-family: var(--mono); color: var(--accent);">${escapeHtml(key)}</span>
1427
+ </div>
1428
+ <div style="display:flex; gap:6px; font-size:11px;">
1429
+ ${bCount > 0 ? `<span style="color:var(--warn); background:var(--warn-bg); padding:2px 8px; border-radius:12px; font-weight:700;">🔴 ${bCount} Blockers</span>` : ''}
1430
+ ${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>` : ''}
1431
+ </div>
1432
+ `;
1433
+
1434
+ const body = document.createElement('div');
1435
+ body.style.display = 'flex';
1436
+ body.style.flexDirection = 'column';
1437
+ body.style.background = 'var(--bg)';
1438
+
1439
+ for (const b of g.blockers) {
1440
+ const r = createBlockerRow(b);
1441
+ r.style.margin = '0';
1442
+ r.style.borderRadius = '0';
1443
+ r.style.borderBottom = '1px solid var(--border)';
1444
+ r.style.borderLeft = '4px solid var(--warn)';
1445
+ r.style.boxShadow = 'none';
1446
+ body.appendChild(r);
1447
+ }
1448
+ for (const t of g.tasks) {
1449
+ const r = createTaskRow(t);
1450
+ r.style.margin = '0';
1451
+ r.style.borderRadius = '0';
1452
+ r.style.borderBottom = '1px solid var(--border)';
1453
+ r.style.borderLeft = '4px solid transparent';
1454
+ r.style.boxShadow = 'none';
1455
+ body.appendChild(r);
1456
+ }
1457
+
1458
+ let isOpen = true;
1459
+ head.onclick = () => {
1460
+ isOpen = !isOpen;
1461
+ body.style.display = isOpen ? 'flex' : 'none';
1462
+ const svg = head.querySelector('svg');
1463
+ if (svg) svg.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
1464
+ };
1465
+
1466
+ swimlane.appendChild(head);
1467
+ swimlane.appendChild(body);
1468
+ el.appendChild(swimlane);
1128
1469
  }
1129
1470
  }
1130
1471
 
1131
1472
  async function refreshToday() {
1132
1473
  try {
1133
1474
  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 })
1475
+ api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 50 }),
1476
+ api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 50 })
1136
1477
  ]);
1137
- renderTasks((t && t.tasks) || []);
1138
- renderBlockers((b && b.blockers) || []);
1478
+ renderSwimlanes((t && t.tasks) || [], (b && b.blockers) || []);
1139
1479
  refreshBlockersInsights();
1140
1480
  } catch (e) {
1141
1481
  // keep silent in background refresh
@@ -1355,25 +1695,34 @@
1355
1695
  const nodes = new vis.DataSet(r.nodes);
1356
1696
  const edges = new vis.DataSet(r.edges);
1357
1697
 
1698
+ const style = getComputedStyle(document.body);
1699
+ const bg = style.getPropertyValue('--bg').trim() || '#0f1115';
1700
+ const paper = style.getPropertyValue('--paper').trim() || '#14161a';
1701
+ const primary = style.getPropertyValue('--primary').trim() || '#38bdf8';
1702
+ const accent = style.getPropertyValue('--accent').trim() || '#2dd4bf';
1703
+ const text = style.getPropertyValue('--text').trim() || '#f1f5f9';
1704
+ const line2 = style.getPropertyValue('--line2').trim() || 'rgba(255,255,255,0.12)';
1705
+ const sansFont = style.getPropertyValue('--sans').trim() || 'Inter, sans-serif';
1706
+
1358
1707
  const data = { nodes, edges };
1359
1708
  const options = {
1360
1709
  nodes: {
1361
1710
  shape: 'dot',
1362
1711
  size: 16,
1363
- font: { color: 'var(--fg)', face: 'var(--sans)' },
1712
+ font: { color: text, face: sansFont },
1364
1713
  borderWidth: 2
1365
1714
  },
1366
1715
  edges: {
1367
- color: { color: 'var(--border)', highlight: 'var(--brand)' },
1716
+ color: { color: line2, highlight: primary },
1368
1717
  width: 1,
1369
1718
  smooth: { type: 'continuous' }
1370
1719
  },
1371
1720
  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 }
1721
+ project: { color: { background: paper, border: primary } },
1722
+ task: { color: { background: paper, border: accent }, size: 10 },
1723
+ blocker: { color: { background: 'rgba(239, 68, 68, 0.15)', border: '#ef4444' }, size: 14, shape: 'triangle' },
1724
+ tag: { color: { background: bg, border: line2 }, size: 8, font: { size: 10, color: text } },
1725
+ unassigned: { color: { background: bg, border: line2 }, size: 10 }
1377
1726
  },
1378
1727
  physics: {
1379
1728
  forceAtlas2Based: { gravitationalConstant: -26, centralGravity: 0.005, springLength: 230, springConstant: 0.18 },
@@ -1388,6 +1737,32 @@
1388
1737
  state.networkInstance.destroy();
1389
1738
  }
1390
1739
  state.networkInstance = new vis.Network(el, data, options);
1740
+
1741
+ // Make nodes actionable
1742
+ state.networkInstance.on('click', (params) => {
1743
+ if (params.nodes.length > 0) {
1744
+ const nodeId = String(params.nodes[0]);
1745
+ if (nodeId.startsWith('tag:')) {
1746
+ const tag = nodeId.replace('tag:', '');
1747
+ const railP = $('railProjects');
1748
+ const pf = $('projectsFilter');
1749
+ if (railP && pf) {
1750
+ pf.value = tag;
1751
+ railP.click();
1752
+ setTimeout(() => { if (window.renderProjects) renderProjects(); }, 50);
1753
+ }
1754
+ } else if (nodeId.startsWith('task:') || nodeId.startsWith('blocker:')) {
1755
+ const railD = $('railDashboard');
1756
+ if (railD) railD.click();
1757
+ } else if (nodeId !== 'unassigned_tasks' && nodeId !== 'unassigned_blockers') {
1758
+ // It's a project slug
1759
+ setTimelineProject(nodeId);
1760
+ const railT = $('railTimeline');
1761
+ if (railT) railT.click();
1762
+ }
1763
+ }
1764
+ });
1765
+
1391
1766
  } catch (e) {
1392
1767
  console.error('Failed to load graph data', e);
1393
1768
  }
@@ -1884,6 +2259,8 @@
1884
2259
  window.renderReportsList = renderReportsList;
1885
2260
  window.renderReportsPage = renderReportsPage;
1886
2261
  window.refreshReportsPage = refreshReportsPage;
2262
+ window.setReportsTab = setReportsTab;
2263
+ window.togglePinReport = togglePinReport;
1887
2264
  window.refreshProjects = refreshProjects;
1888
2265
  window.refreshTimeline = refreshTimeline;
1889
2266
  window.refreshGraph = refreshGraph;
@@ -1911,4 +2288,5 @@
1911
2288
  window.runSuggestedReports = runSuggestedReports;
1912
2289
  window.exportChatObsidian = exportChatObsidian;
1913
2290
  window.askFreya = askFreya;
2291
+ window.askFreyaInline = askFreyaInline;
1914
2292
  })();