@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.
- package/.agent/rules/freya/agents/coach.mdc +7 -16
- package/.agent/rules/freya/agents/ingestor.mdc +1 -89
- package/.agent/rules/freya/agents/master.mdc +3 -0
- package/.agent/rules/freya/agents/oracle.mdc +7 -23
- package/cli/web-ui.css +965 -182
- package/cli/web-ui.js +551 -173
- package/cli/web.js +863 -536
- package/package.json +7 -4
- package/scripts/build-vector-index.js +85 -0
- package/scripts/export-obsidian.js +6 -16
- package/scripts/generate-blockers-report.js +5 -17
- package/scripts/generate-daily-summary.js +25 -58
- package/scripts/generate-executive-report.js +22 -204
- package/scripts/generate-sm-weekly-report.js +27 -92
- package/scripts/lib/DataLayer.js +92 -0
- package/scripts/lib/DataManager.js +198 -0
- package/scripts/lib/Embedder.js +59 -0
- package/scripts/lib/schema.js +23 -0
- package/scripts/migrate-v1-v2.js +184 -0
- package/scripts/validate-data.js +48 -51
- package/scripts/validate-structure.js +12 -58
- package/templates/base/scripts/build-vector-index.js +85 -0
- package/templates/base/scripts/export-obsidian.js +143 -0
- package/templates/base/scripts/generate-daily-summary.js +25 -58
- package/templates/base/scripts/generate-executive-report.js +14 -225
- package/templates/base/scripts/generate-sm-weekly-report.js +9 -91
- package/templates/base/scripts/index/build-index.js +13 -0
- package/templates/base/scripts/index/update-index.js +15 -0
- package/templates/base/scripts/lib/DataLayer.js +92 -0
- package/templates/base/scripts/lib/DataManager.js +198 -0
- package/templates/base/scripts/lib/Embedder.js +59 -0
- package/templates/base/scripts/lib/index-utils.js +407 -0
- package/templates/base/scripts/lib/schema.js +23 -0
- package/templates/base/scripts/lib/search-utils.js +183 -0
- package/templates/base/scripts/migrate-v1-v2.js +184 -0
- package/templates/base/scripts/validate-data.js +48 -51
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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"
|
|
677
|
-
+ '<
|
|
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:
|
|
681
|
-
+ '<div
|
|
682
|
-
+ '<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
|
|
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"
|
|
783
|
-
+ '<
|
|
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
|
|
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:
|
|
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
|
-
|
|
979
|
-
if (isReports) {
|
|
1242
|
+
if (!isDashboard) {
|
|
980
1243
|
window.location.href = '/';
|
|
981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1002
|
-
if (!isTimeline) window.location.href = '/timeline';
|
|
1267
|
+
if (curPage !== 'timeline') window.location.href = '/timeline';
|
|
1003
1268
|
};
|
|
1004
1269
|
}
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
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 (
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
|
1058
|
-
const el = $('
|
|
1320
|
+
function renderSwimlanes(tasks, blockers) {
|
|
1321
|
+
const el = $('swimlaneContainer');
|
|
1059
1322
|
if (!el) return;
|
|
1060
1323
|
el.innerHTML = '';
|
|
1061
|
-
|
|
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:
|
|
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
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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:
|
|
1135
|
-
api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit:
|
|
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
|
-
|
|
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:
|
|
1712
|
+
font: { color: text, face: sansFont },
|
|
1364
1713
|
borderWidth: 2
|
|
1365
1714
|
},
|
|
1366
1715
|
edges: {
|
|
1367
|
-
color: { color:
|
|
1716
|
+
color: { color: line2, highlight: primary },
|
|
1368
1717
|
width: 1,
|
|
1369
1718
|
smooth: { type: 'continuous' }
|
|
1370
1719
|
},
|
|
1371
1720
|
groups: {
|
|
1372
|
-
project: { color: { background:
|
|
1373
|
-
task: { color: { background:
|
|
1374
|
-
blocker: { color: { background: '
|
|
1375
|
-
tag: { color: { background:
|
|
1376
|
-
unassigned: { color: { background:
|
|
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
|
})();
|