@phren/cli 0.0.19 → 0.0.20
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/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/generated/memory-ui-graph.browser.js +326 -0
- package/mcp/dist/memory-ui-assets.js +1 -1
- package/mcp/dist/memory-ui-graph.js +36 -2224
- package/mcp/dist/memory-ui-graph.runtime.js +326 -0
- package/mcp/dist/memory-ui-page.js +10 -11
- package/mcp/dist/memory-ui-scripts.js +870 -0
- package/mcp/dist/memory-ui-server.js +29 -1
- package/package.json +5 -1
|
@@ -1567,6 +1567,368 @@ export function renderSearchScript(authToken) {
|
|
|
1567
1567
|
};
|
|
1568
1568
|
})();`;
|
|
1569
1569
|
}
|
|
1570
|
+
// renderGraphPopupScript removed — replaced by renderGraphHostScript + sigma popover
|
|
1571
|
+
function __removed_renderGraphPopupScript() {
|
|
1572
|
+
return `(function() {
|
|
1573
|
+
var popup = document.getElementById('graph-popup');
|
|
1574
|
+
var popupCard = document.getElementById('graph-popup-card');
|
|
1575
|
+
var popupLabel = document.getElementById('graph-popup-label');
|
|
1576
|
+
var popupTitle = document.getElementById('graph-popup-title');
|
|
1577
|
+
var popupMeta = document.getElementById('graph-popup-meta');
|
|
1578
|
+
var popupBody = document.getElementById('graph-popup-body');
|
|
1579
|
+
var popupClose = document.getElementById('graph-popup-close');
|
|
1580
|
+
var esc = window._phrenEsc || function(s) { return String(s); };
|
|
1581
|
+
var authUrl = window._phrenAuthUrl || function(path) { return path; };
|
|
1582
|
+
var authBody = window._phrenAuthBody || function(body) { return body; };
|
|
1583
|
+
var fetchCsrfToken = window._phrenFetchCsrfToken || function(cb) { cb(null); };
|
|
1584
|
+
var currentDetail = null;
|
|
1585
|
+
var isEditing = false;
|
|
1586
|
+
|
|
1587
|
+
function showToast(msg, type) {
|
|
1588
|
+
var container = document.getElementById('toast-container');
|
|
1589
|
+
if (!container) return;
|
|
1590
|
+
var toast = document.createElement('div');
|
|
1591
|
+
toast.className = 'toast' + (type ? ' ' + type : '');
|
|
1592
|
+
toast.textContent = msg;
|
|
1593
|
+
container.appendChild(toast);
|
|
1594
|
+
setTimeout(function() {
|
|
1595
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
|
1596
|
+
}, 2800);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function typeLabel(kind) {
|
|
1600
|
+
if (kind === 'finding') return 'Finding';
|
|
1601
|
+
if (kind === 'task') return 'Task';
|
|
1602
|
+
if (kind === 'entity') return 'Fragment';
|
|
1603
|
+
if (kind === 'reference') return 'Reference';
|
|
1604
|
+
return 'Project';
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function popupPoint(point) {
|
|
1608
|
+
return point || { x: 28, y: 28 };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function renderMeta(detail) {
|
|
1612
|
+
if (!popupMeta) return;
|
|
1613
|
+
var parts = [];
|
|
1614
|
+
if (detail.project) parts.push('<span class="graph-pill">' + esc(detail.project) + '</span>');
|
|
1615
|
+
if (detail.section) parts.push('<span class="graph-pill">' + esc(detail.section) + '</span>');
|
|
1616
|
+
if (detail.priority) parts.push('<span class="graph-pill">' + esc(detail.priority + ' priority') + '</span>');
|
|
1617
|
+
if (detail.entityType) parts.push('<span class="graph-pill">' + esc(detail.entityType) + '</span>');
|
|
1618
|
+
if (detail.topicLabel) parts.push('<span class="graph-pill">' + esc(detail.topicLabel) + '</span>');
|
|
1619
|
+
if (detail.health) parts.push('<span class="graph-pill graph-pill-' + esc(detail.health) + '">' + esc(detail.health) + '</span>');
|
|
1620
|
+
popupMeta.innerHTML = parts.join('');
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function statLine(label, value) {
|
|
1624
|
+
return '<div class="graph-popup-line"><span>' + esc(label) + '</span><strong>' + esc(value) + '</strong></div>';
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function renderDocs(detail) {
|
|
1628
|
+
if (!detail.refDocs || !detail.refDocs.length) return '';
|
|
1629
|
+
var docs = detail.refDocs.slice(0, 8).map(function(ref) {
|
|
1630
|
+
var doc = typeof ref === 'string' ? ref : (ref.doc || '');
|
|
1631
|
+
return '<div class="graph-popup-doc">' + esc(doc) + '</div>';
|
|
1632
|
+
}).join('');
|
|
1633
|
+
return '<div class="graph-popup-section"><h4>Linked docs</h4><div class="graph-popup-docs">' + docs + '</div></div>';
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function renderProject(detail) {
|
|
1637
|
+
var counts = detail.connections || {};
|
|
1638
|
+
return '' +
|
|
1639
|
+
'<div class="graph-popup-summary">' + esc(detail.fullLabel || detail.label || detail.id) + '</div>' +
|
|
1640
|
+
'<div class="graph-popup-stats">' +
|
|
1641
|
+
statLine('Neighbors', counts.total || 0) +
|
|
1642
|
+
statLine('Findings', counts.findings || 0) +
|
|
1643
|
+
statLine('Tasks', counts.tasks || 0) +
|
|
1644
|
+
statLine('Fragments', counts.entities || 0) +
|
|
1645
|
+
statLine('References', counts.references || 0) +
|
|
1646
|
+
'</div>';
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function renderFinding(detail) {
|
|
1650
|
+
return '' +
|
|
1651
|
+
'<div class="graph-popup-summary" id="graph-popup-text">' + esc(detail.fullLabel || detail.label) + '</div>' +
|
|
1652
|
+
'<div class="graph-popup-actions">' +
|
|
1653
|
+
'<button class="btn btn-sm" data-graph-action="edit">Edit</button>' +
|
|
1654
|
+
'<button class="btn btn-sm" data-graph-action="delete" style="border-color:var(--danger);color:var(--danger)">Delete</button>' +
|
|
1655
|
+
'</div>' +
|
|
1656
|
+
renderDocs(detail);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function renderTask(detail) {
|
|
1660
|
+
var section = detail.section || 'Queue';
|
|
1661
|
+
var priority = detail.priority || 'none';
|
|
1662
|
+
return '' +
|
|
1663
|
+
'<div class="graph-popup-summary" id="graph-popup-text">' + esc(detail.fullLabel || detail.label) + '</div>' +
|
|
1664
|
+
'<div class="graph-popup-section"><h4>Status</h4><div class="graph-popup-actions">' +
|
|
1665
|
+
'<button class="btn btn-sm' + (section === 'Active' ? ' active' : '') + '" data-graph-action="task-status" data-status="Active">Active</button>' +
|
|
1666
|
+
'<button class="btn btn-sm' + (section === 'Queue' ? ' active' : '') + '" data-graph-action="task-status" data-status="Queue">Queue</button>' +
|
|
1667
|
+
'<button class="btn btn-sm" data-graph-action="task-status" data-status="Done">Done</button>' +
|
|
1668
|
+
'</div></div>' +
|
|
1669
|
+
'<div class="graph-popup-section"><h4>Priority</h4><div class="graph-popup-actions">' +
|
|
1670
|
+
'<button class="btn btn-sm' + (priority === 'high' ? ' active' : '') + '" data-graph-action="task-priority" data-priority="high">High</button>' +
|
|
1671
|
+
'<button class="btn btn-sm' + (priority === 'medium' ? ' active' : '') + '" data-graph-action="task-priority" data-priority="medium">Medium</button>' +
|
|
1672
|
+
'<button class="btn btn-sm' + (priority === 'low' ? ' active' : '') + '" data-graph-action="task-priority" data-priority="low">Low</button>' +
|
|
1673
|
+
'</div></div>' +
|
|
1674
|
+
'<div class="graph-popup-actions">' +
|
|
1675
|
+
'<button class="btn btn-sm" data-graph-action="edit">Edit</button>' +
|
|
1676
|
+
'<button class="btn btn-sm" data-graph-action="delete" style="border-color:var(--danger);color:var(--danger)">Delete</button>' +
|
|
1677
|
+
'</div>';
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function renderEntity(detail) {
|
|
1681
|
+
var projects = (detail.connectedProjects || []).map(function(project) {
|
|
1682
|
+
return '<span class="graph-pill">' + esc(project) + '</span>';
|
|
1683
|
+
}).join('');
|
|
1684
|
+
return '' +
|
|
1685
|
+
'<div class="graph-popup-summary">' + esc(detail.fullLabel || detail.label) + '</div>' +
|
|
1686
|
+
'<div class="graph-popup-stats">' +
|
|
1687
|
+
statLine('References', detail.refCount || 0) +
|
|
1688
|
+
statLine('Projects', (detail.connectedProjects || []).length) +
|
|
1689
|
+
statLine('Neighbors', (detail.connections && detail.connections.total) || 0) +
|
|
1690
|
+
'</div>' +
|
|
1691
|
+
(projects ? '<div class="graph-popup-section"><h4>Projects</h4><div class="graph-popup-pills">' + projects + '</div></div>' : '') +
|
|
1692
|
+
renderDocs(detail);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function renderReference(detail) {
|
|
1696
|
+
return '' +
|
|
1697
|
+
'<div class="graph-popup-summary">' + esc(detail.fullLabel || detail.label) + '</div>' +
|
|
1698
|
+
'<div class="graph-popup-stats">' +
|
|
1699
|
+
statLine('Project', detail.project || 'shared') +
|
|
1700
|
+
statLine('Neighbors', (detail.connections && detail.connections.total) || 0) +
|
|
1701
|
+
'</div>' +
|
|
1702
|
+
renderDocs(detail);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function renderEditor(detail) {
|
|
1706
|
+
return '' +
|
|
1707
|
+
'<div class="graph-popup-editor">' +
|
|
1708
|
+
'<textarea id="graph-popup-editor-input">' + esc(detail.fullLabel || detail.label) + '</textarea>' +
|
|
1709
|
+
'<div class="graph-popup-actions">' +
|
|
1710
|
+
'<button class="btn btn-sm btn-primary" data-graph-action="save-edit">Save</button>' +
|
|
1711
|
+
'<button class="btn btn-sm" data-graph-action="cancel-edit">Cancel</button>' +
|
|
1712
|
+
'</div>' +
|
|
1713
|
+
'</div>';
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function renderBody(detail) {
|
|
1717
|
+
if (!popupBody) return;
|
|
1718
|
+
if (isEditing && (detail.kind === 'finding' || detail.kind === 'task')) {
|
|
1719
|
+
popupBody.innerHTML = renderEditor(detail);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (detail.kind === 'project') popupBody.innerHTML = renderProject(detail);
|
|
1723
|
+
else if (detail.kind === 'finding') popupBody.innerHTML = renderFinding(detail);
|
|
1724
|
+
else if (detail.kind === 'task') popupBody.innerHTML = renderTask(detail);
|
|
1725
|
+
else if (detail.kind === 'entity') popupBody.innerHTML = renderEntity(detail);
|
|
1726
|
+
else popupBody.innerHTML = renderReference(detail);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function positionPopup(point) {
|
|
1730
|
+
if (!popupCard) return;
|
|
1731
|
+
var container = document.querySelector('#tab-graph .graph-container');
|
|
1732
|
+
if (!container) return;
|
|
1733
|
+
var rect = container.getBoundingClientRect();
|
|
1734
|
+
var width = popupCard.offsetWidth || 360;
|
|
1735
|
+
var height = popupCard.offsetHeight || 260;
|
|
1736
|
+
var safe = popupPoint(point);
|
|
1737
|
+
var left = Math.max(14, Math.min(safe.x + 18, rect.width - width - 14));
|
|
1738
|
+
var top = Math.max(14, Math.min(safe.y + 18, rect.height - height - 14));
|
|
1739
|
+
popupCard.style.left = left + 'px';
|
|
1740
|
+
popupCard.style.top = top + 'px';
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function renderPopup(detail, point) {
|
|
1744
|
+
if (!popup || !popupLabel || !popupTitle) return;
|
|
1745
|
+
currentDetail = detail;
|
|
1746
|
+
popupLabel.textContent = typeLabel(detail.kind);
|
|
1747
|
+
popupTitle.textContent = detail.label || detail.fullLabel || detail.id;
|
|
1748
|
+
renderMeta(detail);
|
|
1749
|
+
renderBody(detail);
|
|
1750
|
+
popup.classList.add('open');
|
|
1751
|
+
requestAnimationFrame(function() { positionPopup(point); });
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function closePopup(skipSelectionClear) {
|
|
1755
|
+
currentDetail = null;
|
|
1756
|
+
isEditing = false;
|
|
1757
|
+
if (popup) popup.classList.remove('open');
|
|
1758
|
+
if (!skipSelectionClear && typeof window.graphClearSelection === 'function') window.graphClearSelection();
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function refetchGraph(keepNodeId) {
|
|
1762
|
+
if (typeof window.loadGraph !== 'function') return;
|
|
1763
|
+
window.loadGraph();
|
|
1764
|
+
if (keepNodeId && window.phrenGraph && typeof window.phrenGraph.selectNode === 'function') {
|
|
1765
|
+
var attempts = 0;
|
|
1766
|
+
var timer = setInterval(function() {
|
|
1767
|
+
attempts++;
|
|
1768
|
+
if (window.phrenGraph.selectNode(keepNodeId) || attempts > 18) clearInterval(timer);
|
|
1769
|
+
}, 120);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function postForm(url, method, body, onOk) {
|
|
1774
|
+
fetchCsrfToken(function(csrfToken) {
|
|
1775
|
+
var nextBody = authBody(body);
|
|
1776
|
+
if (csrfToken) nextBody += '&_csrf=' + encodeURIComponent(csrfToken);
|
|
1777
|
+
fetch(url, {
|
|
1778
|
+
method: method,
|
|
1779
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
1780
|
+
body: nextBody
|
|
1781
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
1782
|
+
if (!data.ok) throw new Error(data.error || 'Request failed');
|
|
1783
|
+
onOk(data);
|
|
1784
|
+
}).catch(function(err) {
|
|
1785
|
+
showToast(String((err && err.message) || err || 'Request failed'), 'err');
|
|
1786
|
+
});
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function saveFindingEdit(nextText) {
|
|
1791
|
+
postForm(authUrl('/api/findings/' + encodeURIComponent(currentDetail.project)), 'PUT',
|
|
1792
|
+
'old_text=' + encodeURIComponent(currentDetail.fullLabel || currentDetail.label) + '&new_text=' + encodeURIComponent(nextText),
|
|
1793
|
+
function() {
|
|
1794
|
+
currentDetail.fullLabel = nextText;
|
|
1795
|
+
currentDetail.label = nextText.length > 55 ? nextText.slice(0, 52) + '...' : nextText;
|
|
1796
|
+
isEditing = false;
|
|
1797
|
+
renderPopup(currentDetail);
|
|
1798
|
+
refetchGraph(currentDetail.id);
|
|
1799
|
+
showToast('Finding updated', 'ok');
|
|
1800
|
+
}
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function saveTaskEdit(nextText) {
|
|
1805
|
+
postForm('/api/tasks/update', 'POST',
|
|
1806
|
+
'project=' + encodeURIComponent(currentDetail.project) + '&item=' + encodeURIComponent(currentDetail.fullLabel || currentDetail.label) + '&text=' + encodeURIComponent(nextText),
|
|
1807
|
+
function() {
|
|
1808
|
+
currentDetail.fullLabel = nextText;
|
|
1809
|
+
currentDetail.label = nextText.length > 55 ? nextText.slice(0, 52) + '...' : nextText;
|
|
1810
|
+
isEditing = false;
|
|
1811
|
+
renderPopup(currentDetail);
|
|
1812
|
+
refetchGraph(currentDetail.id);
|
|
1813
|
+
showToast('Task updated', 'ok');
|
|
1814
|
+
}
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function updateTask(payload) {
|
|
1819
|
+
var body = 'project=' + encodeURIComponent(currentDetail.project) + '&item=' + encodeURIComponent(currentDetail.fullLabel || currentDetail.label);
|
|
1820
|
+
Object.keys(payload).forEach(function(key) {
|
|
1821
|
+
body += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(payload[key]);
|
|
1822
|
+
});
|
|
1823
|
+
postForm('/api/tasks/update', 'POST', body, function() {
|
|
1824
|
+
if (payload.section) currentDetail.section = payload.section;
|
|
1825
|
+
if (payload.priority) currentDetail.priority = payload.priority;
|
|
1826
|
+
renderPopup(currentDetail);
|
|
1827
|
+
refetchGraph(payload.section === 'Done' ? null : currentDetail.id);
|
|
1828
|
+
if (payload.section === 'Done') closePopup(true);
|
|
1829
|
+
showToast('Task updated', 'ok');
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function completeTask() {
|
|
1834
|
+
postForm('/api/tasks/complete', 'POST',
|
|
1835
|
+
'project=' + encodeURIComponent(currentDetail.project) + '&item=' + encodeURIComponent(currentDetail.fullLabel || currentDetail.label),
|
|
1836
|
+
function() {
|
|
1837
|
+
closePopup(true);
|
|
1838
|
+
refetchGraph(null);
|
|
1839
|
+
showToast('Task completed', 'ok');
|
|
1840
|
+
}
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function deleteCurrent() {
|
|
1845
|
+
if (!currentDetail) return;
|
|
1846
|
+
if (currentDetail.kind === 'finding') {
|
|
1847
|
+
postForm(authUrl('/api/findings/' + encodeURIComponent(currentDetail.project)), 'DELETE',
|
|
1848
|
+
'text=' + encodeURIComponent(currentDetail.fullLabel || currentDetail.label),
|
|
1849
|
+
function() {
|
|
1850
|
+
closePopup(true);
|
|
1851
|
+
refetchGraph(null);
|
|
1852
|
+
showToast('Finding deleted', 'ok');
|
|
1853
|
+
}
|
|
1854
|
+
);
|
|
1855
|
+
} else if (currentDetail.kind === 'task') {
|
|
1856
|
+
postForm('/api/tasks/remove', 'POST',
|
|
1857
|
+
'project=' + encodeURIComponent(currentDetail.project) + '&item=' + encodeURIComponent(currentDetail.fullLabel || currentDetail.label),
|
|
1858
|
+
function() {
|
|
1859
|
+
closePopup(true);
|
|
1860
|
+
refetchGraph(null);
|
|
1861
|
+
showToast('Task deleted', 'ok');
|
|
1862
|
+
}
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (popupClose) popupClose.addEventListener('click', function(e) {
|
|
1868
|
+
e.preventDefault();
|
|
1869
|
+
e.stopPropagation();
|
|
1870
|
+
closePopup(false);
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
if (popupCard) {
|
|
1874
|
+
popupCard.addEventListener('click', function(e) {
|
|
1875
|
+
var target = e.target;
|
|
1876
|
+
if (!target || typeof target.closest !== 'function' || !currentDetail) return;
|
|
1877
|
+
var actionEl = target.closest('[data-graph-action]');
|
|
1878
|
+
if (!actionEl) return;
|
|
1879
|
+
var action = actionEl.getAttribute('data-graph-action');
|
|
1880
|
+
if (action === 'edit') {
|
|
1881
|
+
isEditing = true;
|
|
1882
|
+
renderPopup(currentDetail);
|
|
1883
|
+
} else if (action === 'cancel-edit') {
|
|
1884
|
+
isEditing = false;
|
|
1885
|
+
renderPopup(currentDetail);
|
|
1886
|
+
} else if (action === 'save-edit') {
|
|
1887
|
+
var input = document.getElementById('graph-popup-editor-input');
|
|
1888
|
+
var nextText = input ? input.value.trim() : '';
|
|
1889
|
+
if (!nextText) {
|
|
1890
|
+
showToast('Text cannot be empty', 'err');
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (currentDetail.kind === 'finding') saveFindingEdit(nextText);
|
|
1894
|
+
else if (currentDetail.kind === 'task') saveTaskEdit(nextText);
|
|
1895
|
+
} else if (action === 'delete') {
|
|
1896
|
+
deleteCurrent();
|
|
1897
|
+
} else if (action === 'task-status') {
|
|
1898
|
+
var status = actionEl.getAttribute('data-status') || 'Queue';
|
|
1899
|
+
if (status === 'Done') completeTask();
|
|
1900
|
+
else updateTask({ section: status });
|
|
1901
|
+
} else if (action === 'task-priority') {
|
|
1902
|
+
updateTask({ priority: actionEl.getAttribute('data-priority') || 'medium' });
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
document.addEventListener('pointerdown', function(e) {
|
|
1908
|
+
if (!popup || !popup.classList.contains('open')) return;
|
|
1909
|
+
var target = e.target;
|
|
1910
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
1911
|
+
if (popupCard && popupCard.contains(target)) return;
|
|
1912
|
+
if (target.closest('.graph-filters') || target.closest('.graph-controls')) return;
|
|
1913
|
+
closePopup(false);
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
if (window.phrenGraph && typeof window.phrenGraph.onNodeSelect === 'function') {
|
|
1917
|
+
window.phrenGraph.onNodeSelect(function(detail, point) {
|
|
1918
|
+
isEditing = false;
|
|
1919
|
+
renderPopup(detail, point);
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
if (window.phrenGraph && typeof window.phrenGraph.onSelectionClear === 'function') {
|
|
1924
|
+
window.phrenGraph.onSelectionClear(function() {
|
|
1925
|
+
currentDetail = null;
|
|
1926
|
+
isEditing = false;
|
|
1927
|
+
if (popup) popup.classList.remove('open');
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
})();`;
|
|
1931
|
+
}
|
|
1570
1932
|
export function renderEventWiringScript() {
|
|
1571
1933
|
return `(function() {
|
|
1572
1934
|
// --- Navigation tabs ---
|
|
@@ -1655,3 +2017,511 @@ export function renderEventWiringScript() {
|
|
|
1655
2017
|
}
|
|
1656
2018
|
})();`;
|
|
1657
2019
|
}
|
|
2020
|
+
export function renderGraphHostScript() {
|
|
2021
|
+
return `(function() {
|
|
2022
|
+
var currentNode = null;
|
|
2023
|
+
var editMode = null;
|
|
2024
|
+
|
|
2025
|
+
function graphApi() {
|
|
2026
|
+
return window.phrenGraph || null;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function esc(value) {
|
|
2030
|
+
return String(value)
|
|
2031
|
+
.replace(/&/g, '&')
|
|
2032
|
+
.replace(/</g, '<')
|
|
2033
|
+
.replace(/>/g, '>')
|
|
2034
|
+
.replace(/"/g, '"');
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function graphData() {
|
|
2038
|
+
var api = graphApi();
|
|
2039
|
+
return api && api.getData ? api.getData() : { nodes: [], links: [], topics: [], total: 0 };
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function authToken() {
|
|
2043
|
+
try {
|
|
2044
|
+
return new URL(window.location.href).searchParams.get('_auth') || '';
|
|
2045
|
+
} catch {
|
|
2046
|
+
return '';
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function authUrl(path) {
|
|
2051
|
+
var token = authToken();
|
|
2052
|
+
if (!token) return path;
|
|
2053
|
+
return path + (path.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(token);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function graphToast(message, type) {
|
|
2057
|
+
var container = document.getElementById('toast-container');
|
|
2058
|
+
if (!container) return;
|
|
2059
|
+
var toast = document.createElement('div');
|
|
2060
|
+
toast.className = 'toast' + (type ? ' ' + type : '');
|
|
2061
|
+
toast.textContent = message;
|
|
2062
|
+
container.appendChild(toast);
|
|
2063
|
+
setTimeout(function() {
|
|
2064
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
|
2065
|
+
}, 2600);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function fetchCsrfToken() {
|
|
2069
|
+
return fetch(authUrl('/api/csrf-token')).then(function(r) { return r.json(); }).then(function(data) {
|
|
2070
|
+
return data && data.ok ? (data.token || null) : null;
|
|
2071
|
+
}).catch(function() { return null; });
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function formBody(fields, csrfToken) {
|
|
2075
|
+
var parts = [];
|
|
2076
|
+
Object.keys(fields).forEach(function(key) {
|
|
2077
|
+
var value = fields[key];
|
|
2078
|
+
if (value === undefined || value === null) return;
|
|
2079
|
+
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
|
|
2080
|
+
});
|
|
2081
|
+
if (csrfToken) parts.push('_csrf=' + encodeURIComponent(csrfToken));
|
|
2082
|
+
return parts.join('&');
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function graphRequest(path, method, fields) {
|
|
2086
|
+
return fetchCsrfToken().then(function(csrfToken) {
|
|
2087
|
+
return fetch(authUrl(path), {
|
|
2088
|
+
method: method,
|
|
2089
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
2090
|
+
body: formBody(fields || {}, csrfToken)
|
|
2091
|
+
}).then(function(r) { return r.json(); });
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function hidePopover() {
|
|
2096
|
+
currentNode = null;
|
|
2097
|
+
editMode = null;
|
|
2098
|
+
var popover = document.getElementById('graph-node-popover');
|
|
2099
|
+
if (popover) popover.style.display = 'none';
|
|
2100
|
+
if (typeof window.graphClearSelection === 'function') window.graphClearSelection();
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function positionPopover(x, y) {
|
|
2104
|
+
var popover = document.getElementById('graph-node-popover');
|
|
2105
|
+
var card = document.getElementById('graph-node-popover-card');
|
|
2106
|
+
var container = document.querySelector('#tab-graph .graph-container');
|
|
2107
|
+
if (!popover || !card || !container) return;
|
|
2108
|
+
popover.style.display = 'block';
|
|
2109
|
+
popover.style.visibility = 'hidden';
|
|
2110
|
+
requestAnimationFrame(function() {
|
|
2111
|
+
var containerRect = container.getBoundingClientRect();
|
|
2112
|
+
var cardRect = card.getBoundingClientRect();
|
|
2113
|
+
var left = Math.min(Math.max(12, x + 18), Math.max(12, containerRect.width - cardRect.width - 12));
|
|
2114
|
+
var top = Math.min(Math.max(12, y + 18), Math.max(12, containerRect.height - cardRect.height - 12));
|
|
2115
|
+
popover.style.left = left + 'px';
|
|
2116
|
+
popover.style.top = top + 'px';
|
|
2117
|
+
popover.style.visibility = 'visible';
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function currentPopoverPoint() {
|
|
2122
|
+
var popover = document.getElementById('graph-node-popover');
|
|
2123
|
+
return {
|
|
2124
|
+
x: popover ? parseFloat(popover.style.left || '24') : 24,
|
|
2125
|
+
y: popover ? parseFloat(popover.style.top || '24') : 24
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function neighborIds(nodeId) {
|
|
2130
|
+
var data = graphData();
|
|
2131
|
+
var ids = [];
|
|
2132
|
+
(data.links || []).forEach(function(link) {
|
|
2133
|
+
if (link.source === nodeId) ids.push(link.target);
|
|
2134
|
+
else if (link.target === nodeId) ids.push(link.source);
|
|
2135
|
+
});
|
|
2136
|
+
return ids;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function nodeMap() {
|
|
2140
|
+
var data = graphData();
|
|
2141
|
+
var map = {};
|
|
2142
|
+
(data.nodes || []).forEach(function(node) { map[node.id] = node; });
|
|
2143
|
+
return map;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function projectCounts(node) {
|
|
2147
|
+
var map = nodeMap();
|
|
2148
|
+
var counts = { finding: 0, task: 0, entity: 0, reference: 0, other: 0 };
|
|
2149
|
+
neighborIds(node.id).forEach(function(id) {
|
|
2150
|
+
var neighbor = map[id];
|
|
2151
|
+
if (!neighbor) return;
|
|
2152
|
+
var kind = neighbor.kind || 'other';
|
|
2153
|
+
if (counts[kind] === undefined) counts.other++;
|
|
2154
|
+
else counts[kind]++;
|
|
2155
|
+
});
|
|
2156
|
+
return counts;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function kindLabel(node) {
|
|
2160
|
+
if (!node) return '';
|
|
2161
|
+
if (node.kind === 'entity') return node.entityType ? 'Fragment · ' + node.entityType : 'Fragment';
|
|
2162
|
+
if (node.kind === 'reference') return 'Reference';
|
|
2163
|
+
if (node.kind === 'task') return 'Task';
|
|
2164
|
+
if (node.kind === 'project') return 'Project';
|
|
2165
|
+
if (node.kind === 'finding') return node.topicLabel ? 'Finding · ' + node.topicLabel : 'Finding';
|
|
2166
|
+
return node.kind || 'Node';
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function chip(text, accent) {
|
|
2170
|
+
var border = accent ? 'var(--accent)' : 'var(--border)';
|
|
2171
|
+
var bg = accent ? 'var(--accent-dim)' : 'var(--surface-raised)';
|
|
2172
|
+
return '<span style="display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;border:1px solid ' + border + ';background:' + bg + ';font-size:11px;color:var(--ink)">' + esc(text) + '</span>';
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function scoreLine(node) {
|
|
2176
|
+
var score = typeof node.qualityScore === 'number' ? Math.round(node.qualityScore * 100) : null;
|
|
2177
|
+
return score ? chip('Quality ' + score, false) : '';
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
function docChip(doc) {
|
|
2181
|
+
var border = 'var(--border)';
|
|
2182
|
+
var bg = 'var(--surface-raised)';
|
|
2183
|
+
return '<span data-doc-click="' + esc(doc) + '" style="display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;border:1px solid ' + border + ';background:' + bg + ';font-size:11px;color:var(--accent);cursor:pointer;text-decoration:underline dotted" title="Search for ' + esc(doc) + '">' + esc(doc) + '</span>';
|
|
2184
|
+
}
|
|
2185
|
+
function docsList(node) {
|
|
2186
|
+
var docs = (node.refDocs || []).map(function(ref) { return ref.doc; });
|
|
2187
|
+
if (!docs.length) return '';
|
|
2188
|
+
return '<div style="display:flex;flex-wrap:wrap;gap:8px">' + docs.slice(0, 12).map(function(doc) { return docChip(doc); }).join('') + '</div>';
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function renderView(node) {
|
|
2192
|
+
var title = node.displayLabel || node.label || node.tooltipLabel || node.id;
|
|
2193
|
+
var meta = [kindLabel(node)];
|
|
2194
|
+
if (node.projectName) meta.push(node.projectName);
|
|
2195
|
+
if (node.kind === 'task' && node.section) meta.push(node.section);
|
|
2196
|
+
if (node.kind === 'task' && node.priority) meta.push('Priority ' + node.priority);
|
|
2197
|
+
if (node.kind === 'finding' && node.topicLabel) meta.push(node.topicLabel);
|
|
2198
|
+
|
|
2199
|
+
var header = '<div style="display:flex;flex-direction:column;gap:8px;padding-right:44px"><div style="font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)">' + esc(kindLabel(node)) + '</div><div style="font-size:var(--text-lg);font-weight:600;line-height:1.2">' + esc(title) + '</div><div style="display:flex;flex-wrap:wrap;gap:8px">' + meta.filter(Boolean).map(function(item, index) { return chip(item, index === 0); }).join('') + scoreLine(node) + '</div></div>';
|
|
2200
|
+
|
|
2201
|
+
var body = '';
|
|
2202
|
+
var actions = [];
|
|
2203
|
+
|
|
2204
|
+
if (node.kind === 'project') {
|
|
2205
|
+
var counts = projectCounts(node);
|
|
2206
|
+
body += '<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px">';
|
|
2207
|
+
body += '<div class="card" style="padding:12px"><div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">Findings</div><div style="font-size:var(--text-lg);font-weight:600;margin-top:4px">' + counts.finding + '</div></div>';
|
|
2208
|
+
body += '<div class="card" style="padding:12px"><div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">Tasks</div><div style="font-size:var(--text-lg);font-weight:600;margin-top:4px">' + counts.task + '</div></div>';
|
|
2209
|
+
body += '<div class="card" style="padding:12px"><div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">Fragments</div><div style="font-size:var(--text-lg);font-weight:600;margin-top:4px">' + counts.entity + '</div></div>';
|
|
2210
|
+
body += '<div class="card" style="padding:12px"><div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">References</div><div style="font-size:var(--text-lg);font-weight:600;margin-top:4px">' + counts.reference + '</div></div>';
|
|
2211
|
+
body += '</div>';
|
|
2212
|
+
} else if (node.kind === 'finding') {
|
|
2213
|
+
body += '<div id="graph-node-text" style="white-space:pre-wrap;line-height:1.65;font-size:var(--text-base)">' + esc(node.tooltipLabel || node.fullLabel || title) + '</div>';
|
|
2214
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="edit">Edit</button>');
|
|
2215
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="delete" style="border-color:var(--danger);color:var(--danger)">Delete</button>');
|
|
2216
|
+
} else if (node.kind === 'task') {
|
|
2217
|
+
body += '<div id="graph-node-text" style="white-space:pre-wrap;line-height:1.65;font-size:var(--text-base)">' + esc(node.tooltipLabel || node.fullLabel || title) + '</div>';
|
|
2218
|
+
body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px">';
|
|
2219
|
+
body += chip('Status ' + (node.section || 'Queue'), true);
|
|
2220
|
+
if (node.priority) body += chip('Priority ' + node.priority, false);
|
|
2221
|
+
body += '</div>';
|
|
2222
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="edit">Edit</button>');
|
|
2223
|
+
if ((node.section || '').toLowerCase() !== 'done') actions.push('<button type="button" class="btn btn-sm" data-graph-action="complete">Done</button>');
|
|
2224
|
+
if ((node.section || '').toLowerCase() !== 'active') actions.push('<button type="button" class="btn btn-sm" data-graph-action="move-active">Move to Active</button>');
|
|
2225
|
+
if ((node.section || '').toLowerCase() !== 'queue') actions.push('<button type="button" class="btn btn-sm" data-graph-action="move-queue">Move to Queue</button>');
|
|
2226
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="delete" style="border-color:var(--danger);color:var(--danger)">Delete</button>');
|
|
2227
|
+
} else if (node.kind === 'entity') {
|
|
2228
|
+
if (node.connectedProjects && node.connectedProjects.length) {
|
|
2229
|
+
body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">' + node.connectedProjects.map(function(project) { return chip(project, true); }).join('') + '</div>';
|
|
2230
|
+
}
|
|
2231
|
+
body += docsList(node) || '<div class="text-muted">No linked references.</div>';
|
|
2232
|
+
} else if (node.kind === 'reference') {
|
|
2233
|
+
body += '<div style="white-space:pre-wrap;line-height:1.6;font-size:var(--text-base)">' + esc(node.tooltipLabel || title) + '</div>';
|
|
2234
|
+
body += docsList(node);
|
|
2235
|
+
} else {
|
|
2236
|
+
body += '<div style="white-space:pre-wrap;line-height:1.6;font-size:var(--text-base)">' + esc(node.tooltipLabel || title) + '</div>';
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
return header
|
|
2240
|
+
+ '<div style="display:flex;flex-direction:column;gap:14px;margin-top:16px">' + body + '</div>'
|
|
2241
|
+
+ (actions.length ? '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:18px">' + actions.join('') + '</div>' : '');
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
function renderEdit(node) {
|
|
2245
|
+
var title = node.kind === 'task' ? 'Edit task' : 'Edit finding';
|
|
2246
|
+
var text = node.tooltipLabel || node.fullLabel || node.displayLabel || '';
|
|
2247
|
+
var section = node.section || 'Queue';
|
|
2248
|
+
var priority = node.priority || '';
|
|
2249
|
+
var sectionControls = '';
|
|
2250
|
+
var priorityControls = '';
|
|
2251
|
+
if (node.kind === 'task') {
|
|
2252
|
+
sectionControls = '<label style="display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted)">Status<select id="graph-task-section" style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;background:var(--surface);color:var(--ink)"><option value="Queue"' + (section === 'Queue' ? ' selected' : '') + '>Queue</option><option value="Active"' + (section === 'Active' ? ' selected' : '') + '>Active</option><option value="Done"' + (section === 'Done' ? ' selected' : '') + '>Done</option></select></label>';
|
|
2253
|
+
priorityControls = '<label style="display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted)">Priority<select id="graph-task-priority" style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;background:var(--surface);color:var(--ink)"><option value=""' + (!priority ? ' selected' : '') + '>None</option><option value="high"' + (priority === 'high' ? ' selected' : '') + '>High</option><option value="medium"' + (priority === 'medium' ? ' selected' : '') + '>Medium</option><option value="low"' + (priority === 'low' ? ' selected' : '') + '>Low</option></select></label>';
|
|
2254
|
+
}
|
|
2255
|
+
return '<div style="display:flex;flex-direction:column;gap:8px;padding-right:44px"><div style="font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)">' + esc(title) + '</div><div style="font-size:var(--text-md);font-weight:600">' + esc(node.projectName || kindLabel(node)) + '</div></div>'
|
|
2256
|
+
+ '<div style="display:flex;flex-direction:column;gap:12px;margin-top:16px">'
|
|
2257
|
+
+ '<textarea id="graph-node-editor" style="min-height:180px;width:100%;border:1px solid var(--border);border-radius:12px;padding:12px 14px;background:var(--surface-sunken);color:var(--ink);font:inherit;line-height:1.55;resize:vertical">' + esc(text) + '</textarea>'
|
|
2258
|
+
+ (node.kind === 'task' ? '<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px">' + sectionControls + priorityControls + '</div>' : '')
|
|
2259
|
+
+ '<div style="display:flex;flex-wrap:wrap;gap:8px"><button type="button" class="btn btn-sm" data-graph-action="save-edit">Save</button><button type="button" class="btn btn-sm" data-graph-action="cancel-edit">Cancel</button></div>'
|
|
2260
|
+
+ '</div>';
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function renderPopover(node, x, y) {
|
|
2264
|
+
currentNode = node;
|
|
2265
|
+
var content = document.getElementById('graph-node-content');
|
|
2266
|
+
if (!content || !node) {
|
|
2267
|
+
currentNode = null;
|
|
2268
|
+
editMode = null;
|
|
2269
|
+
var popover = document.getElementById('graph-node-popover');
|
|
2270
|
+
if (popover) popover.style.display = 'none';
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
content.innerHTML = editMode ? renderEdit(node) : renderView(node);
|
|
2274
|
+
bindPopoverActions();
|
|
2275
|
+
positionPopover(x, y);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function matchesNode(node, match) {
|
|
2279
|
+
if (!node || !match) return false;
|
|
2280
|
+
if (match.id && node.id !== match.id) return false;
|
|
2281
|
+
if (match.kind && node.kind !== match.kind) return false;
|
|
2282
|
+
if (match.projectName && node.projectName !== match.projectName) return false;
|
|
2283
|
+
if (match.tooltipLabel && (node.tooltipLabel || node.fullLabel || '') !== match.tooltipLabel) return false;
|
|
2284
|
+
if (match.displayLabel && (node.displayLabel || node.label || '') !== match.displayLabel) return false;
|
|
2285
|
+
return true;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
function reloadGraph(match) {
|
|
2289
|
+
return fetch(authUrl('/api/graph')).then(function(r) {
|
|
2290
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
2291
|
+
return r.json();
|
|
2292
|
+
}).then(function(data) {
|
|
2293
|
+
var api = graphApi();
|
|
2294
|
+
if (!api || !api.mount) return;
|
|
2295
|
+
api.mount(data);
|
|
2296
|
+
if (!match) {
|
|
2297
|
+
hidePopover();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
var next = null;
|
|
2301
|
+
var dataNodes = api.getData ? api.getData().nodes : [];
|
|
2302
|
+
for (var index = 0; index < dataNodes.length; index++) {
|
|
2303
|
+
if (matchesNode(dataNodes[index], match)) {
|
|
2304
|
+
next = dataNodes[index];
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (next && api.focusNode) api.focusNode(next.id);
|
|
2309
|
+
else hidePopover();
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
function saveFindingEdit() {
|
|
2314
|
+
var editor = document.getElementById('graph-node-editor');
|
|
2315
|
+
if (!editor || !currentNode) return;
|
|
2316
|
+
var nextText = editor.value.trim();
|
|
2317
|
+
if (!nextText || nextText === (currentNode.tooltipLabel || currentNode.fullLabel || '').trim()) {
|
|
2318
|
+
editMode = null;
|
|
2319
|
+
var point = currentPopoverPoint();
|
|
2320
|
+
renderPopover(currentNode, point.x, point.y);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
graphRequest('/api/findings/' + encodeURIComponent(currentNode.projectName), 'PUT', {
|
|
2324
|
+
old_text: currentNode.tooltipLabel || currentNode.fullLabel || '',
|
|
2325
|
+
new_text: nextText
|
|
2326
|
+
}).then(function(result) {
|
|
2327
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
|
|
2328
|
+
graphToast('Finding updated', 'ok');
|
|
2329
|
+
editMode = null;
|
|
2330
|
+
return reloadGraph({ kind: 'finding', projectName: currentNode.projectName, tooltipLabel: nextText });
|
|
2331
|
+
}).catch(function(err) {
|
|
2332
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function saveTaskEdit() {
|
|
2337
|
+
var editor = document.getElementById('graph-node-editor');
|
|
2338
|
+
var sectionEl = document.getElementById('graph-task-section');
|
|
2339
|
+
var priorityEl = document.getElementById('graph-task-priority');
|
|
2340
|
+
if (!editor || !currentNode) return;
|
|
2341
|
+
var nextText = editor.value.trim();
|
|
2342
|
+
if (!nextText) {
|
|
2343
|
+
graphToast('Task text cannot be empty', 'err');
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
var updates = { text: nextText };
|
|
2347
|
+
if (sectionEl && sectionEl.value) updates.section = sectionEl.value;
|
|
2348
|
+
if (priorityEl) updates.priority = priorityEl.value;
|
|
2349
|
+
graphRequest('/api/tasks/update', 'POST', {
|
|
2350
|
+
project: currentNode.projectName,
|
|
2351
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
2352
|
+
text: updates.text,
|
|
2353
|
+
section: updates.section || '',
|
|
2354
|
+
priority: updates.priority || ''
|
|
2355
|
+
}).then(function(result) {
|
|
2356
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
|
|
2357
|
+
graphToast('Task updated', 'ok');
|
|
2358
|
+
editMode = null;
|
|
2359
|
+
var nextSection = updates.section || currentNode.section || 'Queue';
|
|
2360
|
+
if (nextSection === 'Done') {
|
|
2361
|
+
hidePopover();
|
|
2362
|
+
return reloadGraph(null);
|
|
2363
|
+
}
|
|
2364
|
+
return reloadGraph({ kind: 'task', projectName: currentNode.projectName, tooltipLabel: nextText });
|
|
2365
|
+
}).catch(function(err) {
|
|
2366
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function deleteCurrentNode() {
|
|
2371
|
+
if (!currentNode) return;
|
|
2372
|
+
if (!confirm('Delete this ' + (currentNode.kind || 'node') + '?')) return;
|
|
2373
|
+
if (currentNode.kind === 'finding') {
|
|
2374
|
+
graphRequest('/api/findings/' + encodeURIComponent(currentNode.projectName), 'DELETE', {
|
|
2375
|
+
text: currentNode.tooltipLabel || currentNode.fullLabel || ''
|
|
2376
|
+
}).then(function(result) {
|
|
2377
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
|
|
2378
|
+
graphToast('Finding deleted', 'ok');
|
|
2379
|
+
return reloadGraph(null);
|
|
2380
|
+
}).catch(function(err) {
|
|
2381
|
+
graphToast('Delete failed: ' + err.message, 'err');
|
|
2382
|
+
});
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
if (currentNode.kind === 'task') {
|
|
2386
|
+
graphRequest('/api/tasks/remove', 'POST', {
|
|
2387
|
+
project: currentNode.projectName,
|
|
2388
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
2389
|
+
}).then(function(result) {
|
|
2390
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
|
|
2391
|
+
graphToast('Task removed', 'ok');
|
|
2392
|
+
return reloadGraph(null);
|
|
2393
|
+
}).catch(function(err) {
|
|
2394
|
+
graphToast('Delete failed: ' + err.message, 'err');
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function completeCurrentTask() {
|
|
2400
|
+
if (!currentNode) return;
|
|
2401
|
+
graphRequest('/api/tasks/complete', 'POST', {
|
|
2402
|
+
project: currentNode.projectName,
|
|
2403
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
2404
|
+
}).then(function(result) {
|
|
2405
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
|
|
2406
|
+
graphToast('Task completed', 'ok');
|
|
2407
|
+
return reloadGraph(null);
|
|
2408
|
+
}).catch(function(err) {
|
|
2409
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
function moveCurrentTask(section) {
|
|
2414
|
+
if (!currentNode) return;
|
|
2415
|
+
graphRequest('/api/tasks/update', 'POST', {
|
|
2416
|
+
project: currentNode.projectName,
|
|
2417
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
2418
|
+
section: section
|
|
2419
|
+
}).then(function(result) {
|
|
2420
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
|
|
2421
|
+
graphToast('Task moved to ' + section, 'ok');
|
|
2422
|
+
return reloadGraph({ kind: 'task', projectName: currentNode.projectName, tooltipLabel: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '' });
|
|
2423
|
+
}).catch(function(err) {
|
|
2424
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
function bindPopoverActions() {
|
|
2429
|
+
var closeBtn = document.getElementById('graph-node-close');
|
|
2430
|
+
if (closeBtn) closeBtn.onclick = hidePopover;
|
|
2431
|
+
|
|
2432
|
+
// Doc reference chips — click to search for the document
|
|
2433
|
+
document.querySelectorAll('[data-doc-click]').forEach(function(chip) {
|
|
2434
|
+
chip.addEventListener('click', function() {
|
|
2435
|
+
var doc = chip.getAttribute('data-doc-click') || '';
|
|
2436
|
+
if (!doc) return;
|
|
2437
|
+
// Search for this doc in the graph by updating the search filter
|
|
2438
|
+
var searchInput = document.querySelector('input[data-search-filter]');
|
|
2439
|
+
if (searchInput) {
|
|
2440
|
+
searchInput.value = doc.replace(/FINDINGS\\.md$/, '').replace(/\\/$/, '');
|
|
2441
|
+
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
document.querySelectorAll('[data-graph-action]').forEach(function(button) {
|
|
2447
|
+
button.addEventListener('click', function() {
|
|
2448
|
+
var action = button.getAttribute('data-graph-action');
|
|
2449
|
+
if (action === 'edit') {
|
|
2450
|
+
editMode = currentNode && (currentNode.kind === 'finding' || currentNode.kind === 'task') ? currentNode.kind : null;
|
|
2451
|
+
var point = currentPopoverPoint();
|
|
2452
|
+
renderPopover(currentNode, point.x, point.y);
|
|
2453
|
+
} else if (action === 'cancel-edit') {
|
|
2454
|
+
editMode = null;
|
|
2455
|
+
var point = currentPopoverPoint();
|
|
2456
|
+
renderPopover(currentNode, point.x, point.y);
|
|
2457
|
+
} else if (action === 'save-edit') {
|
|
2458
|
+
if (editMode === 'task') saveTaskEdit();
|
|
2459
|
+
else saveFindingEdit();
|
|
2460
|
+
} else if (action === 'delete') {
|
|
2461
|
+
deleteCurrentNode();
|
|
2462
|
+
} else if (action === 'complete') {
|
|
2463
|
+
completeCurrentTask();
|
|
2464
|
+
} else if (action === 'move-active') {
|
|
2465
|
+
moveCurrentTask('Active');
|
|
2466
|
+
} else if (action === 'move-queue') {
|
|
2467
|
+
moveCurrentTask('Queue');
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function ensureHostBindings() {
|
|
2474
|
+
var api = graphApi();
|
|
2475
|
+
if (!api || !api.onNodeSelect) return false;
|
|
2476
|
+
api.onNodeSelect(function(node, x, y) {
|
|
2477
|
+
if (!node) {
|
|
2478
|
+
currentNode = null;
|
|
2479
|
+
editMode = null;
|
|
2480
|
+
var popover = document.getElementById('graph-node-popover');
|
|
2481
|
+
if (popover) popover.style.display = 'none';
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
editMode = null;
|
|
2485
|
+
renderPopover(node, x, y);
|
|
2486
|
+
});
|
|
2487
|
+
if (typeof api.onSelectionClear === 'function') {
|
|
2488
|
+
api.onSelectionClear(function() {
|
|
2489
|
+
currentNode = null;
|
|
2490
|
+
editMode = null;
|
|
2491
|
+
var popover = document.getElementById('graph-node-popover');
|
|
2492
|
+
if (popover) popover.style.display = 'none';
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
return true;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function onOutsidePointer(event) {
|
|
2499
|
+
var popover = document.getElementById('graph-node-popover-card');
|
|
2500
|
+
if (!currentNode || !popover) return;
|
|
2501
|
+
if (popover.contains(event.target)) return;
|
|
2502
|
+
hidePopover();
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function onEscape(event) {
|
|
2506
|
+
if (event.key !== 'Escape' || !currentNode) return;
|
|
2507
|
+
if (editMode) {
|
|
2508
|
+
editMode = null;
|
|
2509
|
+
var point = currentPopoverPoint();
|
|
2510
|
+
renderPopover(currentNode, point.x, point.y);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
hidePopover();
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
document.addEventListener('pointerdown', onOutsidePointer);
|
|
2517
|
+
document.addEventListener('keydown', onEscape);
|
|
2518
|
+
|
|
2519
|
+
if (!ensureHostBindings()) {
|
|
2520
|
+
var tries = 0;
|
|
2521
|
+
var timer = setInterval(function() {
|
|
2522
|
+
tries++;
|
|
2523
|
+
if (ensureHostBindings() || tries > 40) clearInterval(timer);
|
|
2524
|
+
}, 100);
|
|
2525
|
+
}
|
|
2526
|
+
})();`;
|
|
2527
|
+
}
|