@phren/cli 0.0.19 → 0.0.21
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/hooks.js +51 -14
- package/mcp/dist/mcp-data.js +4 -5
- package/mcp/dist/memory-ui-assets.js +2 -2
- package/mcp/dist/memory-ui-data.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 +78 -52
- package/mcp/dist/memory-ui-scripts.js +545 -193
- package/mcp/dist/memory-ui-server.js +26 -32
- package/mcp/dist/memory-ui-styles.js +137 -136
- package/mcp/dist/profile-store.js +1 -13
- package/mcp/dist/shared-ollama.js +1 -12
- package/mcp/dist/shell-state-store.js +2 -14
- package/package.json +5 -1
|
@@ -588,9 +588,7 @@ export function renderProjectReferenceEnhancementScript(_authToken) {
|
|
|
588
588
|
});
|
|
589
589
|
})();`;
|
|
590
590
|
}
|
|
591
|
-
|
|
592
|
-
return "";
|
|
593
|
-
}
|
|
591
|
+
// renderReviewQueueEditSyncScript removed — was dead code returning ""
|
|
594
592
|
export function renderTasksAndSettingsScript(authToken) {
|
|
595
593
|
const safeToken = JSON.stringify(authToken).slice(1, -1);
|
|
596
594
|
return `(function() {
|
|
@@ -606,27 +604,11 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
606
604
|
return fetch(url).then(function(r) { return r.json(); });
|
|
607
605
|
}
|
|
608
606
|
|
|
609
|
-
var _taskViewMode = 'list';
|
|
610
|
-
|
|
611
607
|
function priorityBadge(p) {
|
|
612
608
|
if (!p) return '';
|
|
613
|
-
var colors = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' };
|
|
614
|
-
var color = colors[p] || '#6b7280';
|
|
615
609
|
return '<span class="task-priority-badge task-priority-' + esc(p) + '">' + esc(p) + '</span>';
|
|
616
610
|
}
|
|
617
611
|
|
|
618
|
-
function sessionBadge(sessionId) {
|
|
619
|
-
if (!sessionId) return '';
|
|
620
|
-
var short = sessionId.length > 8 ? sessionId.slice(0, 8) : sessionId;
|
|
621
|
-
return '<span class="task-session-badge" title="Session ' + esc(sessionId) + '">' + esc(short) + '</span>';
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function statusChip(section, checked) {
|
|
625
|
-
if (checked || section === 'Done') return '<span class="task-status-chip task-status-done" title="Done"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Done</span>';
|
|
626
|
-
if (section === 'Active') return '<span class="task-status-chip task-status-active" title="In Progress">In Progress</span>';
|
|
627
|
-
return '<span class="task-status-chip task-status-pending" title="Pending">Pending</span>';
|
|
628
|
-
}
|
|
629
|
-
|
|
630
612
|
function projectBadge(proj) {
|
|
631
613
|
return '<span class="task-project-badge">' + esc(proj) + '</span>';
|
|
632
614
|
}
|
|
@@ -733,55 +715,40 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
733
715
|
var container = document.getElementById('tasks-list');
|
|
734
716
|
if (!container) return;
|
|
735
717
|
if (!tasks.length && !doneTasks.length) {
|
|
736
|
-
container.innerHTML = '<div class="task-empty-state"><svg viewBox="0 0 48 48" width="
|
|
718
|
+
container.innerHTML = '<div class="task-empty-state"><svg viewBox="0 0 48 48" width="48" height="48" style="display:block;margin:0 auto 12px"><ellipse cx="24" cy="24" rx="16" ry="15" fill="#7B68AE" opacity="0.25"/><ellipse cx="24" cy="24" rx="12" ry="11.5" fill="#7B68AE" opacity="0.4"/><circle cx="19" cy="22" r="1.5" fill="#2D2255"/><circle cx="29" cy="22" r="1.5" fill="#2D2255"/><path d="M21 28c1 1.2 2.5 1.5 3.5 1.3 1-.2 2-1 2.5-1.3" stroke="#2D2255" stroke-width="1" fill="none" stroke-linecap="round"/></svg><div style="font-size:var(--text-md);font-weight:600;color:var(--ink);margin-bottom:4px">Nothing to do!</div><div style="color:var(--muted);font-size:var(--text-sm)">Add a task to get started.</div></div>';
|
|
737
719
|
return;
|
|
738
720
|
}
|
|
739
721
|
|
|
740
|
-
// Group by priority within sections
|
|
741
722
|
var priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
742
723
|
function sortByPriority(a, b) {
|
|
743
|
-
// Unchecked before checked
|
|
744
|
-
var aChecked = a.checked || a.section === 'Done' ? 1 : 0;
|
|
745
|
-
var bChecked = b.checked || b.section === 'Done' ? 1 : 0;
|
|
746
|
-
if (aChecked !== bChecked) return aChecked - bChecked;
|
|
747
|
-
// Pinned first
|
|
748
724
|
if (a.pinned && !b.pinned) return -1;
|
|
749
725
|
if (!a.pinned && b.pinned) return 1;
|
|
750
|
-
// Then by priority
|
|
751
726
|
var pa = priorityOrder[a.priority] !== undefined ? priorityOrder[a.priority] : 1;
|
|
752
727
|
var pb = priorityOrder[b.priority] !== undefined ? priorityOrder[b.priority] : 1;
|
|
753
728
|
return pa - pb;
|
|
754
729
|
}
|
|
755
730
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
var html = '<div class="task-card' + borderClass + doneClass + '">';
|
|
768
|
-
html += '<div class="task-card-top">';
|
|
769
|
-
html += statusChip(t.section, t.checked);
|
|
770
|
-
html += projectBadge(t.project);
|
|
731
|
+
function isNotDone(t) { return t.section !== 'Done' && !t.checked; }
|
|
732
|
+
|
|
733
|
+
function renderTaskRow(t) {
|
|
734
|
+
var isDone = t.section === 'Done' || t.checked;
|
|
735
|
+
var priClass = t.priority ? 'task-row-priority-' + esc(t.priority) : 'task-row-priority-none';
|
|
736
|
+
var html = '<div class="task-row' + (isDone ? ' task-row-done' : '') + '">';
|
|
737
|
+
html += '<div class="task-row-priority ' + priClass + '"></div>';
|
|
738
|
+
html += '<div class="task-row-content">';
|
|
739
|
+
html += '<span class="task-row-text">' + esc(t.line) + '</span>';
|
|
740
|
+
html += '</div>';
|
|
741
|
+
html += '<div class="task-row-meta">';
|
|
771
742
|
html += pinIndicator(t.pinned);
|
|
772
743
|
html += githubBadge(t.githubIssue, t.githubUrl);
|
|
773
744
|
html += priorityBadge(t.priority);
|
|
774
|
-
html +=
|
|
775
|
-
html += '</div>';
|
|
776
|
-
html += '<div class="task-card-body">';
|
|
777
|
-
html += '<span class="task-card-text">' + esc(t.line) + '</span>';
|
|
778
|
-
if (t.context) html += '<span class="task-card-context">' + esc(t.context) + '</span>';
|
|
745
|
+
html += projectBadge(t.project);
|
|
779
746
|
html += '</div>';
|
|
780
|
-
html += '<div class="task-
|
|
781
|
-
if (
|
|
782
|
-
html += '<button class="task-
|
|
747
|
+
html += '<div class="task-row-actions">';
|
|
748
|
+
if (!isDone) {
|
|
749
|
+
html += '<button class="task-action-btn task-action-complete" data-ts-action="completeTask" data-project="' + esc(t.project) + '" data-item="' + esc(t.line) + '" title="Mark done"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>';
|
|
783
750
|
}
|
|
784
|
-
html += '<button class="task-
|
|
751
|
+
html += '<button class="task-action-btn task-action-delete" data-ts-action="removeTask" data-project="' + esc(t.project) + '" data-item="' + esc(t.line) + '" title="Delete task"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>';
|
|
785
752
|
html += '</div>';
|
|
786
753
|
html += '</div>';
|
|
787
754
|
return html;
|
|
@@ -807,49 +774,42 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
807
774
|
topProjects.forEach(function(p) { html += '<span class="task-summary-project">' + esc(p) + ' (' + projectCounts[p] + ')</span>'; });
|
|
808
775
|
html += '</span>';
|
|
809
776
|
}
|
|
810
|
-
html += '<span class="task-view-toggle" style="margin-left:auto">';
|
|
811
|
-
html += '<button class="task-view-btn' + (_taskViewMode === 'list' ? ' active' : '') + '" data-ts-action="setTaskView" data-mode="list" title="List view"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>';
|
|
812
|
-
html += '<button class="task-view-btn' + (_taskViewMode === 'compact' ? ' active' : '') + '" data-ts-action="setTaskView" data-mode="compact" title="Compact view"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><rect x="1" y="2" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="2" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="1" y="9" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="9" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/></svg></button>';
|
|
813
|
-
html += '</span>';
|
|
814
777
|
html += '</div>';
|
|
815
778
|
|
|
816
779
|
// Add task input at top (only when a specific project is selected)
|
|
817
|
-
|
|
818
|
-
projects.forEach(function(proj) {
|
|
780
|
+
if (projectFilter) {
|
|
819
781
|
html += '<div class="task-add-bar">';
|
|
820
|
-
html += '<input id="task-add-input-' + esc(
|
|
821
|
-
html += '<button class="task-add-btn" data-ts-action="addTask" data-project="' + esc(
|
|
782
|
+
html += '<input id="task-add-input-' + esc(projectFilter) + '" type="text" class="task-add-input" placeholder="Add a task to ' + esc(projectFilter) + '\u2026" data-ts-action="addTaskKeydown" data-project="' + esc(projectFilter) + '">';
|
|
783
|
+
html += '<button class="task-add-btn" data-ts-action="addTask" data-project="' + esc(projectFilter) + '"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> Add</button>';
|
|
822
784
|
html += '</div>';
|
|
823
|
-
}
|
|
785
|
+
}
|
|
824
786
|
|
|
825
|
-
//
|
|
826
|
-
|
|
787
|
+
// Group by section: Active first, then Queue
|
|
788
|
+
var activeTasks = tasks.filter(function(t) { return t.section === 'Active' && isNotDone(t); }).sort(sortByPriority);
|
|
789
|
+
var queueTasks = tasks.filter(function(t) { return t.section !== 'Active' && isNotDone(t); }).sort(sortByPriority);
|
|
790
|
+
|
|
791
|
+
function renderSection(title, items) {
|
|
827
792
|
if (!items.length) return '';
|
|
828
|
-
var
|
|
829
|
-
|
|
830
|
-
shtml += '<div class="task-
|
|
831
|
-
shtml +=
|
|
832
|
-
items.forEach(function(t) { shtml += renderTaskCard(t); });
|
|
793
|
+
var shtml = '<div class="task-section-group">';
|
|
794
|
+
shtml += '<div class="task-section-header">' + title + ' <span class="task-section-count">' + items.length + '</span></div>';
|
|
795
|
+
shtml += '<div class="task-list">';
|
|
796
|
+
items.forEach(function(t) { shtml += renderTaskRow(t); });
|
|
833
797
|
shtml += '</div></div>';
|
|
834
798
|
return shtml;
|
|
835
799
|
}
|
|
836
800
|
|
|
837
|
-
html += renderSection('
|
|
838
|
-
html += renderSection('
|
|
839
|
-
html += renderSection('Low Priority', low, '<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="#6b7280"/></svg>');
|
|
840
|
-
if (noPriority.length) {
|
|
841
|
-
html += renderSection('Tasks', noPriority, '<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="var(--accent)"/></svg>');
|
|
842
|
-
}
|
|
801
|
+
html += renderSection('Active', activeTasks);
|
|
802
|
+
html += renderSection('Queue', queueTasks);
|
|
843
803
|
|
|
844
804
|
// Done section (collapsible)
|
|
845
|
-
var allDone = showDone ?
|
|
805
|
+
var allDone = showDone ? tasks.filter(function(t) { return t.section === 'Done' || t.checked; }) : doneTasks;
|
|
846
806
|
if (allDone.length) {
|
|
847
807
|
html += '<div class="task-done-section">';
|
|
848
808
|
html += '<button class="task-done-toggle" data-ts-action="toggleDoneSection">';
|
|
849
809
|
html += '<span class="task-toggle-arrow">\u25B6</span> Completed <span class="task-section-count">' + allDone.length + '</span></button>';
|
|
850
810
|
html += '<div class="task-done-list" style="display:none">';
|
|
851
|
-
html += '<div class="task-
|
|
852
|
-
allDone.forEach(function(t) { html +=
|
|
811
|
+
html += '<div class="task-list">';
|
|
812
|
+
allDone.forEach(function(t) { html += renderTaskRow(t); });
|
|
853
813
|
html += '</div></div></div>';
|
|
854
814
|
}
|
|
855
815
|
|
|
@@ -1172,122 +1132,13 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1172
1132
|
var _origSwitchTab = window.switchTab;
|
|
1173
1133
|
var _tasksLoaded = false;
|
|
1174
1134
|
var _settingsLoaded = false;
|
|
1175
|
-
var _sessionsLoaded = false;
|
|
1176
1135
|
window.switchTab = function(tab) {
|
|
1177
1136
|
if (typeof _origSwitchTab === 'function') _origSwitchTab(tab);
|
|
1178
1137
|
if (tab === 'tasks' && !_tasksLoaded) { _tasksLoaded = true; loadTasks(); }
|
|
1179
1138
|
if (tab === 'settings' && !_settingsLoaded) { _settingsLoaded = true; loadSettings(); }
|
|
1180
|
-
if (tab === 'sessions' && !_sessionsLoaded) { _sessionsLoaded = true; loadSessions(); }
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
var _sessionsData = [];
|
|
1184
|
-
window.loadSessions = function() {
|
|
1185
|
-
var projectFilter = document.getElementById('sessions-filter-project');
|
|
1186
|
-
var project = projectFilter ? projectFilter.value : '';
|
|
1187
|
-
var apiUrl = _tsAuthToken ? tsAuthUrl('/api/sessions') : '/api/sessions';
|
|
1188
|
-
if (project) apiUrl += (apiUrl.includes('?') ? '&' : '?') + 'project=' + encodeURIComponent(project);
|
|
1189
|
-
loadJson(apiUrl).then(function(data) {
|
|
1190
|
-
if (!data.ok) throw new Error(data.error || 'Failed to load sessions');
|
|
1191
|
-
_sessionsData = data.sessions || [];
|
|
1192
|
-
renderSessionsList(_sessionsData);
|
|
1193
|
-
// Populate project filter
|
|
1194
|
-
if (projectFilter && projectFilter.options.length <= 1) {
|
|
1195
|
-
var projects = {};
|
|
1196
|
-
_sessionsData.forEach(function(s) { if (s.project) projects[s.project] = true; });
|
|
1197
|
-
Object.keys(projects).sort().forEach(function(p) {
|
|
1198
|
-
var opt = document.createElement('option');
|
|
1199
|
-
opt.value = p; opt.textContent = p;
|
|
1200
|
-
projectFilter.appendChild(opt);
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
}).catch(function(err) {
|
|
1204
|
-
var list = document.getElementById('sessions-list');
|
|
1205
|
-
if (list) list.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">' + esc(err.message) + '</div>';
|
|
1206
|
-
});
|
|
1207
|
-
};
|
|
1208
|
-
|
|
1209
|
-
function renderSessionsList(sessions) {
|
|
1210
|
-
var list = document.getElementById('sessions-list');
|
|
1211
|
-
var detail = document.getElementById('session-detail');
|
|
1212
|
-
var countEl = document.getElementById('sessions-count');
|
|
1213
|
-
if (detail) detail.style.display = 'none';
|
|
1214
|
-
if (countEl) countEl.textContent = sessions.length + ' session(s)';
|
|
1215
|
-
if (!list) return;
|
|
1216
|
-
if (sessions.length === 0) {
|
|
1217
|
-
list.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center"><svg viewBox="0 0 32 32" width="32" height="32" style="display:block;margin:0 auto 12px"><ellipse cx="16" cy="16" rx="10" ry="9.5" fill="#7B68AE" opacity="0.4"/><path d="M12 15l1-1 1 1-1 1z" fill="#2D2255"/><path d="M18 15l1-1 1 1-1 1z" fill="#2D2255"/><path d="M15 18.5c0.3-0.3 0.7-0.3 1 0" stroke="#2D2255" stroke-width="0.6" fill="none"/></svg>No sessions found.</div>';
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
list.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">' +
|
|
1221
|
-
'<thead><tr style="border-bottom:1px solid var(--border);text-align:left">' +
|
|
1222
|
-
'<th style="padding:8px">Session</th><th style="padding:8px">Project</th><th style="padding:8px">Started</th>' +
|
|
1223
|
-
'<th style="padding:8px">Ended</th><th style="padding:8px">Duration</th><th style="padding:8px">Findings</th></tr></thead><tbody>' +
|
|
1224
|
-
sessions.map(function(s) {
|
|
1225
|
-
var id = s.sessionId.slice(0, 8);
|
|
1226
|
-
var startDate = (s.startedAt || '').slice(0, 16).replace('T', ' ');
|
|
1227
|
-
var endDate = s.endedAt ? (s.endedAt || '').slice(0, 16).replace('T', ' ') : '<span style="color:var(--green)">active</span>';
|
|
1228
|
-
var dur = s.durationMins != null ? s.durationMins + 'm' : '—';
|
|
1229
|
-
var summarySnip = s.summary ? '<div class="text-muted" style="font-size:var(--text-xs);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(s.summary.slice(0, 80)) + '</div>' : '';
|
|
1230
|
-
return '<tr style="border-bottom:1px solid var(--border);cursor:pointer" data-ts-action="showSessionDetail" data-session-id="' + esc(s.sessionId) + '">' +
|
|
1231
|
-
'<td style="padding:8px;font-family:monospace">' + esc(id) + summarySnip + '</td>' +
|
|
1232
|
-
'<td style="padding:8px">' + esc(s.project || '—') + '</td>' +
|
|
1233
|
-
'<td style="padding:8px">' + esc(startDate) + '</td>' +
|
|
1234
|
-
'<td style="padding:8px">' + endDate + '</td>' +
|
|
1235
|
-
'<td style="padding:8px">' + esc(dur) + '</td>' +
|
|
1236
|
-
'<td style="padding:8px">' + (s.findingsAdded || 0) + '</td></tr>';
|
|
1237
|
-
}).join('') + '</tbody></table>';
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
window.showSessionDetail = function(sessionId) {
|
|
1241
|
-
var apiUrl = _tsAuthToken ? tsAuthUrl('/api/sessions') : '/api/sessions';
|
|
1242
|
-
apiUrl += (apiUrl.includes('?') ? '&' : '?') + 'sessionId=' + encodeURIComponent(sessionId);
|
|
1243
|
-
var list = document.getElementById('sessions-list');
|
|
1244
|
-
var detail = document.getElementById('session-detail');
|
|
1245
|
-
if (list) list.style.display = 'none';
|
|
1246
|
-
if (detail) { detail.style.display = 'block'; detail.innerHTML = '<div style="padding:20px;color:var(--muted)">Loading session...</div>'; }
|
|
1247
|
-
loadJson(apiUrl).then(function(data) {
|
|
1248
|
-
if (!data.ok) throw new Error(data.error || 'Session not found');
|
|
1249
|
-
var s = data.session;
|
|
1250
|
-
var findings = data.findings || [];
|
|
1251
|
-
var tasks = data.tasks || [];
|
|
1252
|
-
var date = (s.startedAt || '').slice(0, 16).replace('T', ' ');
|
|
1253
|
-
var dur = s.durationMins != null ? s.durationMins + ' min' : '—';
|
|
1254
|
-
var html = '<div style="margin-bottom:12px"><button class="btn btn-sm" data-ts-action="backToSessionsList">← Back</button></div>' +
|
|
1255
|
-
'<div class="card" style="margin-bottom:16px"><div class="card-body">' +
|
|
1256
|
-
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px">' +
|
|
1257
|
-
'<div><strong>Session</strong><div class="text-muted" style="font-family:monospace">' + esc(s.sessionId.slice(0, 8)) + '</div></div>' +
|
|
1258
|
-
'<div><strong>Project</strong><div class="text-muted">' + esc(s.project || '—') + '</div></div>' +
|
|
1259
|
-
'<div><strong>Date</strong><div class="text-muted">' + esc(date) + '</div></div>' +
|
|
1260
|
-
'<div><strong>Duration</strong><div class="text-muted">' + esc(dur) + '</div></div>' +
|
|
1261
|
-
'<div><strong>Findings</strong><div class="text-muted">' + findings.length + '</div></div>' +
|
|
1262
|
-
'<div><strong>Tasks</strong><div class="text-muted">' + tasks.length + '</div></div>' +
|
|
1263
|
-
'<div><strong>Status</strong><div class="text-muted">' + esc(s.status) + '</div></div>' +
|
|
1264
|
-
'</div>' +
|
|
1265
|
-
(s.summary ? '<div style="margin-top:12px;padding:12px;background:var(--bg);border-radius:var(--radius-sm)"><strong>Summary</strong><div style="margin-top:4px">' + esc(s.summary) + '</div></div>' : '') +
|
|
1266
|
-
'</div></div>';
|
|
1267
|
-
if (findings.length > 0) {
|
|
1268
|
-
html += '<div class="card" style="margin-bottom:16px"><div class="card-header"><h3>Findings (' + findings.length + ')</h3></div><div class="card-body"><ul style="margin:0;padding-left:20px">';
|
|
1269
|
-
findings.forEach(function(f) { html += '<li style="margin-bottom:4px"><span class="text-muted">[' + esc(f.project) + ']</span> ' + esc(f.text) + '</li>'; });
|
|
1270
|
-
html += '</ul></div></div>';
|
|
1271
|
-
}
|
|
1272
|
-
if (tasks.length > 0) {
|
|
1273
|
-
html += '<div class="card" style="margin-bottom:16px"><div class="card-header"><h3>Tasks (' + tasks.length + ')</h3></div><div class="card-body"><ul style="margin:0;padding-left:20px">';
|
|
1274
|
-
tasks.forEach(function(t) { html += '<li style="margin-bottom:4px"><span class="text-muted">[' + esc(t.project) + '/' + esc(t.section) + ']</span> ' + esc(t.text) + '</li>'; });
|
|
1275
|
-
html += '</ul></div></div>';
|
|
1276
|
-
}
|
|
1277
|
-
if (detail) detail.innerHTML = html;
|
|
1278
|
-
}).catch(function(err) {
|
|
1279
|
-
if (detail) detail.innerHTML = '<div style="padding:20px;color:var(--muted)">' + esc(err.message) + '</div>';
|
|
1280
|
-
});
|
|
1281
|
-
};
|
|
1282
|
-
|
|
1283
|
-
window.backToSessionsList = function() {
|
|
1284
|
-
var list = document.getElementById('sessions-list');
|
|
1285
|
-
var detail = document.getElementById('session-detail');
|
|
1286
|
-
if (list) list.style.display = 'block';
|
|
1287
|
-
if (detail) detail.style.display = 'none';
|
|
1288
1139
|
};
|
|
1289
1140
|
|
|
1290
|
-
// Event delegation for dynamically generated tasks/settings
|
|
1141
|
+
// Event delegation for dynamically generated tasks/settings UI
|
|
1291
1142
|
document.addEventListener('click', function(e) {
|
|
1292
1143
|
var target = e.target;
|
|
1293
1144
|
if (!target || typeof target.closest !== 'function') return;
|
|
@@ -1298,15 +1149,12 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1298
1149
|
else if (action === 'completeTask') { completeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1299
1150
|
else if (action === 'removeTask') { removeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1300
1151
|
else if (action === 'addTask') { addTaskFromUi(actionEl.getAttribute('data-project')); }
|
|
1301
|
-
else if (action === 'setTaskView') { _taskViewMode = actionEl.getAttribute('data-mode') || 'list'; filterTasks(); }
|
|
1302
1152
|
else if (action === 'setFindingSensitivity') { setFindingSensitivity(actionEl.getAttribute('data-level')); }
|
|
1303
1153
|
else if (action === 'toggleAutoCapture') { setAutoCapture(actionEl.getAttribute('data-enabled') !== 'true'); }
|
|
1304
1154
|
else if (action === 'setTaskMode') { setTaskMode(actionEl.getAttribute('data-mode')); }
|
|
1305
1155
|
else if (action === 'setProactivity') { setProactivity(actionEl.getAttribute('data-level')); }
|
|
1306
1156
|
else if (action === 'toggleMcpEnabled') { setMcpEnabled(actionEl.getAttribute('data-enabled') !== 'true'); }
|
|
1307
1157
|
else if (action === 'toggleIntegrationTool') { toggleIntegrationTool(actionEl.getAttribute('data-tool')); }
|
|
1308
|
-
else if (action === 'showSessionDetail') { showSessionDetail(actionEl.getAttribute('data-session-id')); }
|
|
1309
|
-
else if (action === 'backToSessionsList') { backToSessionsList(); }
|
|
1310
1158
|
else if (action === 'setProjectFindingSensitivity') {
|
|
1311
1159
|
var proj = getSettingsProject();
|
|
1312
1160
|
var level = actionEl.getAttribute('data-level');
|
|
@@ -1625,10 +1473,6 @@ export function renderEventWiringScript() {
|
|
|
1625
1473
|
var tasksFilterSection = document.getElementById('tasks-filter-section');
|
|
1626
1474
|
if (tasksFilterSection) tasksFilterSection.addEventListener('change', function() { filterTasks(); });
|
|
1627
1475
|
|
|
1628
|
-
// --- Sessions filter ---
|
|
1629
|
-
var sessionsFilterProject = document.getElementById('sessions-filter-project');
|
|
1630
|
-
if (sessionsFilterProject) sessionsFilterProject.addEventListener('change', function() { loadSessions(); });
|
|
1631
|
-
|
|
1632
1476
|
// --- Mascot click animation ---
|
|
1633
1477
|
var mascotSvg = document.querySelector('.header-brand svg');
|
|
1634
1478
|
if (mascotSvg) {
|
|
@@ -1655,3 +1499,511 @@ export function renderEventWiringScript() {
|
|
|
1655
1499
|
}
|
|
1656
1500
|
})();`;
|
|
1657
1501
|
}
|
|
1502
|
+
export function renderGraphHostScript() {
|
|
1503
|
+
return `(function() {
|
|
1504
|
+
var currentNode = null;
|
|
1505
|
+
var editMode = null;
|
|
1506
|
+
|
|
1507
|
+
function graphApi() {
|
|
1508
|
+
return window.phrenGraph || null;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function esc(value) {
|
|
1512
|
+
return String(value)
|
|
1513
|
+
.replace(/&/g, '&')
|
|
1514
|
+
.replace(/</g, '<')
|
|
1515
|
+
.replace(/>/g, '>')
|
|
1516
|
+
.replace(/"/g, '"');
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function graphData() {
|
|
1520
|
+
var api = graphApi();
|
|
1521
|
+
return api && api.getData ? api.getData() : { nodes: [], links: [], topics: [], total: 0 };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function authToken() {
|
|
1525
|
+
try {
|
|
1526
|
+
return new URL(window.location.href).searchParams.get('_auth') || '';
|
|
1527
|
+
} catch {
|
|
1528
|
+
return '';
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function authUrl(path) {
|
|
1533
|
+
var token = authToken();
|
|
1534
|
+
if (!token) return path;
|
|
1535
|
+
return path + (path.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(token);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function graphToast(message, type) {
|
|
1539
|
+
var container = document.getElementById('toast-container');
|
|
1540
|
+
if (!container) return;
|
|
1541
|
+
var toast = document.createElement('div');
|
|
1542
|
+
toast.className = 'toast' + (type ? ' ' + type : '');
|
|
1543
|
+
toast.textContent = message;
|
|
1544
|
+
container.appendChild(toast);
|
|
1545
|
+
setTimeout(function() {
|
|
1546
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
|
1547
|
+
}, 2600);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function fetchCsrfToken() {
|
|
1551
|
+
return fetch(authUrl('/api/csrf-token')).then(function(r) { return r.json(); }).then(function(data) {
|
|
1552
|
+
return data && data.ok ? (data.token || null) : null;
|
|
1553
|
+
}).catch(function() { return null; });
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function formBody(fields, csrfToken) {
|
|
1557
|
+
var parts = [];
|
|
1558
|
+
Object.keys(fields).forEach(function(key) {
|
|
1559
|
+
var value = fields[key];
|
|
1560
|
+
if (value === undefined || value === null) return;
|
|
1561
|
+
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
|
|
1562
|
+
});
|
|
1563
|
+
if (csrfToken) parts.push('_csrf=' + encodeURIComponent(csrfToken));
|
|
1564
|
+
return parts.join('&');
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function graphRequest(path, method, fields) {
|
|
1568
|
+
return fetchCsrfToken().then(function(csrfToken) {
|
|
1569
|
+
return fetch(authUrl(path), {
|
|
1570
|
+
method: method,
|
|
1571
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
1572
|
+
body: formBody(fields || {}, csrfToken)
|
|
1573
|
+
}).then(function(r) { return r.json(); });
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function hidePopover() {
|
|
1578
|
+
currentNode = null;
|
|
1579
|
+
editMode = null;
|
|
1580
|
+
var popover = document.getElementById('graph-node-popover');
|
|
1581
|
+
if (popover) popover.style.display = 'none';
|
|
1582
|
+
if (typeof window.graphClearSelection === 'function') window.graphClearSelection();
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function positionPopover(x, y) {
|
|
1586
|
+
var popover = document.getElementById('graph-node-popover');
|
|
1587
|
+
var card = document.getElementById('graph-node-popover-card');
|
|
1588
|
+
var container = document.querySelector('#tab-graph .graph-container');
|
|
1589
|
+
if (!popover || !card || !container) return;
|
|
1590
|
+
popover.style.display = 'block';
|
|
1591
|
+
popover.style.visibility = 'hidden';
|
|
1592
|
+
requestAnimationFrame(function() {
|
|
1593
|
+
var containerRect = container.getBoundingClientRect();
|
|
1594
|
+
var cardRect = card.getBoundingClientRect();
|
|
1595
|
+
var left = Math.min(Math.max(12, x + 18), Math.max(12, containerRect.width - cardRect.width - 12));
|
|
1596
|
+
var top = Math.min(Math.max(12, y + 18), Math.max(12, containerRect.height - cardRect.height - 12));
|
|
1597
|
+
popover.style.left = left + 'px';
|
|
1598
|
+
popover.style.top = top + 'px';
|
|
1599
|
+
popover.style.visibility = 'visible';
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function currentPopoverPoint() {
|
|
1604
|
+
var popover = document.getElementById('graph-node-popover');
|
|
1605
|
+
return {
|
|
1606
|
+
x: popover ? parseFloat(popover.style.left || '24') : 24,
|
|
1607
|
+
y: popover ? parseFloat(popover.style.top || '24') : 24
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function neighborIds(nodeId) {
|
|
1612
|
+
var data = graphData();
|
|
1613
|
+
var ids = [];
|
|
1614
|
+
(data.links || []).forEach(function(link) {
|
|
1615
|
+
if (link.source === nodeId) ids.push(link.target);
|
|
1616
|
+
else if (link.target === nodeId) ids.push(link.source);
|
|
1617
|
+
});
|
|
1618
|
+
return ids;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function nodeMap() {
|
|
1622
|
+
var data = graphData();
|
|
1623
|
+
var map = {};
|
|
1624
|
+
(data.nodes || []).forEach(function(node) { map[node.id] = node; });
|
|
1625
|
+
return map;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function projectCounts(node) {
|
|
1629
|
+
var map = nodeMap();
|
|
1630
|
+
var counts = { finding: 0, task: 0, entity: 0, reference: 0, other: 0 };
|
|
1631
|
+
neighborIds(node.id).forEach(function(id) {
|
|
1632
|
+
var neighbor = map[id];
|
|
1633
|
+
if (!neighbor) return;
|
|
1634
|
+
var kind = neighbor.kind || 'other';
|
|
1635
|
+
if (counts[kind] === undefined) counts.other++;
|
|
1636
|
+
else counts[kind]++;
|
|
1637
|
+
});
|
|
1638
|
+
return counts;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function kindLabel(node) {
|
|
1642
|
+
if (!node) return '';
|
|
1643
|
+
if (node.kind === 'entity') return node.entityType ? 'Fragment · ' + node.entityType : 'Fragment';
|
|
1644
|
+
if (node.kind === 'reference') return 'Reference';
|
|
1645
|
+
if (node.kind === 'task') return 'Task';
|
|
1646
|
+
if (node.kind === 'project') return 'Project';
|
|
1647
|
+
if (node.kind === 'finding') return node.topicLabel ? 'Finding · ' + node.topicLabel : 'Finding';
|
|
1648
|
+
return node.kind || 'Node';
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function chip(text, accent) {
|
|
1652
|
+
var border = accent ? 'var(--accent)' : 'var(--border)';
|
|
1653
|
+
var bg = accent ? 'var(--accent-dim)' : 'var(--surface-raised)';
|
|
1654
|
+
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>';
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function scoreLine(node) {
|
|
1658
|
+
var score = typeof node.qualityScore === 'number' ? Math.round(node.qualityScore * 100) : null;
|
|
1659
|
+
return score ? chip('Quality ' + score, false) : '';
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function docChip(doc) {
|
|
1663
|
+
var border = 'var(--border)';
|
|
1664
|
+
var bg = 'var(--surface-raised)';
|
|
1665
|
+
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>';
|
|
1666
|
+
}
|
|
1667
|
+
function docsList(node) {
|
|
1668
|
+
var docs = (node.refDocs || []).map(function(ref) { return ref.doc; });
|
|
1669
|
+
if (!docs.length) return '';
|
|
1670
|
+
return '<div style="display:flex;flex-wrap:wrap;gap:8px">' + docs.slice(0, 12).map(function(doc) { return docChip(doc); }).join('') + '</div>';
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function renderView(node) {
|
|
1674
|
+
var title = node.displayLabel || node.label || node.tooltipLabel || node.id;
|
|
1675
|
+
var meta = [kindLabel(node)];
|
|
1676
|
+
if (node.projectName) meta.push(node.projectName);
|
|
1677
|
+
if (node.kind === 'task' && node.section) meta.push(node.section);
|
|
1678
|
+
if (node.kind === 'task' && node.priority) meta.push('Priority ' + node.priority);
|
|
1679
|
+
if (node.kind === 'finding' && node.topicLabel) meta.push(node.topicLabel);
|
|
1680
|
+
|
|
1681
|
+
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>';
|
|
1682
|
+
|
|
1683
|
+
var body = '';
|
|
1684
|
+
var actions = [];
|
|
1685
|
+
|
|
1686
|
+
if (node.kind === 'project') {
|
|
1687
|
+
var counts = projectCounts(node);
|
|
1688
|
+
body += '<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px">';
|
|
1689
|
+
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>';
|
|
1690
|
+
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>';
|
|
1691
|
+
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>';
|
|
1692
|
+
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>';
|
|
1693
|
+
body += '</div>';
|
|
1694
|
+
} else if (node.kind === 'finding') {
|
|
1695
|
+
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>';
|
|
1696
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="edit">Edit</button>');
|
|
1697
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="delete" style="border-color:var(--danger);color:var(--danger)">Delete</button>');
|
|
1698
|
+
} else if (node.kind === 'task') {
|
|
1699
|
+
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>';
|
|
1700
|
+
body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px">';
|
|
1701
|
+
body += chip('Status ' + (node.section || 'Queue'), true);
|
|
1702
|
+
if (node.priority) body += chip('Priority ' + node.priority, false);
|
|
1703
|
+
body += '</div>';
|
|
1704
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="edit">Edit</button>');
|
|
1705
|
+
if ((node.section || '').toLowerCase() !== 'done') actions.push('<button type="button" class="btn btn-sm" data-graph-action="complete">Done</button>');
|
|
1706
|
+
if ((node.section || '').toLowerCase() !== 'active') actions.push('<button type="button" class="btn btn-sm" data-graph-action="move-active">Move to Active</button>');
|
|
1707
|
+
if ((node.section || '').toLowerCase() !== 'queue') actions.push('<button type="button" class="btn btn-sm" data-graph-action="move-queue">Move to Queue</button>');
|
|
1708
|
+
actions.push('<button type="button" class="btn btn-sm" data-graph-action="delete" style="border-color:var(--danger);color:var(--danger)">Delete</button>');
|
|
1709
|
+
} else if (node.kind === 'entity') {
|
|
1710
|
+
if (node.connectedProjects && node.connectedProjects.length) {
|
|
1711
|
+
body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">' + node.connectedProjects.map(function(project) { return chip(project, true); }).join('') + '</div>';
|
|
1712
|
+
}
|
|
1713
|
+
body += docsList(node) || '<div class="text-muted">No linked references.</div>';
|
|
1714
|
+
} else if (node.kind === 'reference') {
|
|
1715
|
+
body += '<div style="white-space:pre-wrap;line-height:1.6;font-size:var(--text-base)">' + esc(node.tooltipLabel || title) + '</div>';
|
|
1716
|
+
body += docsList(node);
|
|
1717
|
+
} else {
|
|
1718
|
+
body += '<div style="white-space:pre-wrap;line-height:1.6;font-size:var(--text-base)">' + esc(node.tooltipLabel || title) + '</div>';
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
return header
|
|
1722
|
+
+ '<div style="display:flex;flex-direction:column;gap:14px;margin-top:16px">' + body + '</div>'
|
|
1723
|
+
+ (actions.length ? '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:18px">' + actions.join('') + '</div>' : '');
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function renderEdit(node) {
|
|
1727
|
+
var title = node.kind === 'task' ? 'Edit task' : 'Edit finding';
|
|
1728
|
+
var text = node.tooltipLabel || node.fullLabel || node.displayLabel || '';
|
|
1729
|
+
var section = node.section || 'Queue';
|
|
1730
|
+
var priority = node.priority || '';
|
|
1731
|
+
var sectionControls = '';
|
|
1732
|
+
var priorityControls = '';
|
|
1733
|
+
if (node.kind === 'task') {
|
|
1734
|
+
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>';
|
|
1735
|
+
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>';
|
|
1736
|
+
}
|
|
1737
|
+
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>'
|
|
1738
|
+
+ '<div style="display:flex;flex-direction:column;gap:12px;margin-top:16px">'
|
|
1739
|
+
+ '<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>'
|
|
1740
|
+
+ (node.kind === 'task' ? '<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px">' + sectionControls + priorityControls + '</div>' : '')
|
|
1741
|
+
+ '<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>'
|
|
1742
|
+
+ '</div>';
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function renderPopover(node, x, y) {
|
|
1746
|
+
currentNode = node;
|
|
1747
|
+
var content = document.getElementById('graph-node-content');
|
|
1748
|
+
if (!content || !node) {
|
|
1749
|
+
currentNode = null;
|
|
1750
|
+
editMode = null;
|
|
1751
|
+
var popover = document.getElementById('graph-node-popover');
|
|
1752
|
+
if (popover) popover.style.display = 'none';
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
content.innerHTML = editMode ? renderEdit(node) : renderView(node);
|
|
1756
|
+
bindPopoverActions();
|
|
1757
|
+
positionPopover(x, y);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function matchesNode(node, match) {
|
|
1761
|
+
if (!node || !match) return false;
|
|
1762
|
+
if (match.id && node.id !== match.id) return false;
|
|
1763
|
+
if (match.kind && node.kind !== match.kind) return false;
|
|
1764
|
+
if (match.projectName && node.projectName !== match.projectName) return false;
|
|
1765
|
+
if (match.tooltipLabel && (node.tooltipLabel || node.fullLabel || '') !== match.tooltipLabel) return false;
|
|
1766
|
+
if (match.displayLabel && (node.displayLabel || node.label || '') !== match.displayLabel) return false;
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function reloadGraph(match) {
|
|
1771
|
+
return fetch(authUrl('/api/graph')).then(function(r) {
|
|
1772
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
1773
|
+
return r.json();
|
|
1774
|
+
}).then(function(data) {
|
|
1775
|
+
var api = graphApi();
|
|
1776
|
+
if (!api || !api.mount) return;
|
|
1777
|
+
api.mount(data);
|
|
1778
|
+
if (!match) {
|
|
1779
|
+
hidePopover();
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
var next = null;
|
|
1783
|
+
var dataNodes = api.getData ? api.getData().nodes : [];
|
|
1784
|
+
for (var index = 0; index < dataNodes.length; index++) {
|
|
1785
|
+
if (matchesNode(dataNodes[index], match)) {
|
|
1786
|
+
next = dataNodes[index];
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (next && api.focusNode) api.focusNode(next.id);
|
|
1791
|
+
else hidePopover();
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function saveFindingEdit() {
|
|
1796
|
+
var editor = document.getElementById('graph-node-editor');
|
|
1797
|
+
if (!editor || !currentNode) return;
|
|
1798
|
+
var nextText = editor.value.trim();
|
|
1799
|
+
if (!nextText || nextText === (currentNode.tooltipLabel || currentNode.fullLabel || '').trim()) {
|
|
1800
|
+
editMode = null;
|
|
1801
|
+
var point = currentPopoverPoint();
|
|
1802
|
+
renderPopover(currentNode, point.x, point.y);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
graphRequest('/api/findings/' + encodeURIComponent(currentNode.projectName), 'PUT', {
|
|
1806
|
+
old_text: currentNode.tooltipLabel || currentNode.fullLabel || '',
|
|
1807
|
+
new_text: nextText
|
|
1808
|
+
}).then(function(result) {
|
|
1809
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
|
|
1810
|
+
graphToast('Finding updated', 'ok');
|
|
1811
|
+
editMode = null;
|
|
1812
|
+
return reloadGraph({ kind: 'finding', projectName: currentNode.projectName, tooltipLabel: nextText });
|
|
1813
|
+
}).catch(function(err) {
|
|
1814
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function saveTaskEdit() {
|
|
1819
|
+
var editor = document.getElementById('graph-node-editor');
|
|
1820
|
+
var sectionEl = document.getElementById('graph-task-section');
|
|
1821
|
+
var priorityEl = document.getElementById('graph-task-priority');
|
|
1822
|
+
if (!editor || !currentNode) return;
|
|
1823
|
+
var nextText = editor.value.trim();
|
|
1824
|
+
if (!nextText) {
|
|
1825
|
+
graphToast('Task text cannot be empty', 'err');
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
var updates = { text: nextText };
|
|
1829
|
+
if (sectionEl && sectionEl.value) updates.section = sectionEl.value;
|
|
1830
|
+
if (priorityEl) updates.priority = priorityEl.value;
|
|
1831
|
+
graphRequest('/api/tasks/update', 'POST', {
|
|
1832
|
+
project: currentNode.projectName,
|
|
1833
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
1834
|
+
text: updates.text,
|
|
1835
|
+
section: updates.section || '',
|
|
1836
|
+
priority: updates.priority || ''
|
|
1837
|
+
}).then(function(result) {
|
|
1838
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
|
|
1839
|
+
graphToast('Task updated', 'ok');
|
|
1840
|
+
editMode = null;
|
|
1841
|
+
var nextSection = updates.section || currentNode.section || 'Queue';
|
|
1842
|
+
if (nextSection === 'Done') {
|
|
1843
|
+
hidePopover();
|
|
1844
|
+
return reloadGraph(null);
|
|
1845
|
+
}
|
|
1846
|
+
return reloadGraph({ kind: 'task', projectName: currentNode.projectName, tooltipLabel: nextText });
|
|
1847
|
+
}).catch(function(err) {
|
|
1848
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function deleteCurrentNode() {
|
|
1853
|
+
if (!currentNode) return;
|
|
1854
|
+
if (!confirm('Delete this ' + (currentNode.kind || 'node') + '?')) return;
|
|
1855
|
+
if (currentNode.kind === 'finding') {
|
|
1856
|
+
graphRequest('/api/findings/' + encodeURIComponent(currentNode.projectName), 'DELETE', {
|
|
1857
|
+
text: currentNode.tooltipLabel || currentNode.fullLabel || ''
|
|
1858
|
+
}).then(function(result) {
|
|
1859
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
|
|
1860
|
+
graphToast('Finding deleted', 'ok');
|
|
1861
|
+
return reloadGraph(null);
|
|
1862
|
+
}).catch(function(err) {
|
|
1863
|
+
graphToast('Delete failed: ' + err.message, 'err');
|
|
1864
|
+
});
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (currentNode.kind === 'task') {
|
|
1868
|
+
graphRequest('/api/tasks/remove', 'POST', {
|
|
1869
|
+
project: currentNode.projectName,
|
|
1870
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
1871
|
+
}).then(function(result) {
|
|
1872
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
|
|
1873
|
+
graphToast('Task removed', 'ok');
|
|
1874
|
+
return reloadGraph(null);
|
|
1875
|
+
}).catch(function(err) {
|
|
1876
|
+
graphToast('Delete failed: ' + err.message, 'err');
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function completeCurrentTask() {
|
|
1882
|
+
if (!currentNode) return;
|
|
1883
|
+
graphRequest('/api/tasks/complete', 'POST', {
|
|
1884
|
+
project: currentNode.projectName,
|
|
1885
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
1886
|
+
}).then(function(result) {
|
|
1887
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
|
|
1888
|
+
graphToast('Task completed', 'ok');
|
|
1889
|
+
return reloadGraph(null);
|
|
1890
|
+
}).catch(function(err) {
|
|
1891
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function moveCurrentTask(section) {
|
|
1896
|
+
if (!currentNode) return;
|
|
1897
|
+
graphRequest('/api/tasks/update', 'POST', {
|
|
1898
|
+
project: currentNode.projectName,
|
|
1899
|
+
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
1900
|
+
section: section
|
|
1901
|
+
}).then(function(result) {
|
|
1902
|
+
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
|
|
1903
|
+
graphToast('Task moved to ' + section, 'ok');
|
|
1904
|
+
return reloadGraph({ kind: 'task', projectName: currentNode.projectName, tooltipLabel: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '' });
|
|
1905
|
+
}).catch(function(err) {
|
|
1906
|
+
graphToast('Update failed: ' + err.message, 'err');
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function bindPopoverActions() {
|
|
1911
|
+
var closeBtn = document.getElementById('graph-node-close');
|
|
1912
|
+
if (closeBtn) closeBtn.onclick = hidePopover;
|
|
1913
|
+
|
|
1914
|
+
// Doc reference chips — click to search for the document
|
|
1915
|
+
document.querySelectorAll('[data-doc-click]').forEach(function(chip) {
|
|
1916
|
+
chip.addEventListener('click', function() {
|
|
1917
|
+
var doc = chip.getAttribute('data-doc-click') || '';
|
|
1918
|
+
if (!doc) return;
|
|
1919
|
+
// Search for this doc in the graph by updating the search filter
|
|
1920
|
+
var searchInput = document.querySelector('input[data-search-filter]');
|
|
1921
|
+
if (searchInput) {
|
|
1922
|
+
searchInput.value = doc.replace(/FINDINGS\\.md$/, '').replace(/\\/$/, '');
|
|
1923
|
+
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
document.querySelectorAll('[data-graph-action]').forEach(function(button) {
|
|
1929
|
+
button.addEventListener('click', function() {
|
|
1930
|
+
var action = button.getAttribute('data-graph-action');
|
|
1931
|
+
if (action === 'edit') {
|
|
1932
|
+
editMode = currentNode && (currentNode.kind === 'finding' || currentNode.kind === 'task') ? currentNode.kind : null;
|
|
1933
|
+
var point = currentPopoverPoint();
|
|
1934
|
+
renderPopover(currentNode, point.x, point.y);
|
|
1935
|
+
} else if (action === 'cancel-edit') {
|
|
1936
|
+
editMode = null;
|
|
1937
|
+
var point = currentPopoverPoint();
|
|
1938
|
+
renderPopover(currentNode, point.x, point.y);
|
|
1939
|
+
} else if (action === 'save-edit') {
|
|
1940
|
+
if (editMode === 'task') saveTaskEdit();
|
|
1941
|
+
else saveFindingEdit();
|
|
1942
|
+
} else if (action === 'delete') {
|
|
1943
|
+
deleteCurrentNode();
|
|
1944
|
+
} else if (action === 'complete') {
|
|
1945
|
+
completeCurrentTask();
|
|
1946
|
+
} else if (action === 'move-active') {
|
|
1947
|
+
moveCurrentTask('Active');
|
|
1948
|
+
} else if (action === 'move-queue') {
|
|
1949
|
+
moveCurrentTask('Queue');
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function ensureHostBindings() {
|
|
1956
|
+
var api = graphApi();
|
|
1957
|
+
if (!api || !api.onNodeSelect) return false;
|
|
1958
|
+
api.onNodeSelect(function(node, x, y) {
|
|
1959
|
+
if (!node) {
|
|
1960
|
+
currentNode = null;
|
|
1961
|
+
editMode = null;
|
|
1962
|
+
var popover = document.getElementById('graph-node-popover');
|
|
1963
|
+
if (popover) popover.style.display = 'none';
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
editMode = null;
|
|
1967
|
+
renderPopover(node, x, y);
|
|
1968
|
+
});
|
|
1969
|
+
if (typeof api.onSelectionClear === 'function') {
|
|
1970
|
+
api.onSelectionClear(function() {
|
|
1971
|
+
currentNode = null;
|
|
1972
|
+
editMode = null;
|
|
1973
|
+
var popover = document.getElementById('graph-node-popover');
|
|
1974
|
+
if (popover) popover.style.display = 'none';
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function onOutsidePointer(event) {
|
|
1981
|
+
var popover = document.getElementById('graph-node-popover-card');
|
|
1982
|
+
if (!currentNode || !popover) return;
|
|
1983
|
+
if (popover.contains(event.target)) return;
|
|
1984
|
+
hidePopover();
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function onEscape(event) {
|
|
1988
|
+
if (event.key !== 'Escape' || !currentNode) return;
|
|
1989
|
+
if (editMode) {
|
|
1990
|
+
editMode = null;
|
|
1991
|
+
var point = currentPopoverPoint();
|
|
1992
|
+
renderPopover(currentNode, point.x, point.y);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
hidePopover();
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
document.addEventListener('pointerdown', onOutsidePointer);
|
|
1999
|
+
document.addEventListener('keydown', onEscape);
|
|
2000
|
+
|
|
2001
|
+
if (!ensureHostBindings()) {
|
|
2002
|
+
var tries = 0;
|
|
2003
|
+
var timer = setInterval(function() {
|
|
2004
|
+
tries++;
|
|
2005
|
+
if (ensureHostBindings() || tries > 40) clearInterval(timer);
|
|
2006
|
+
}, 100);
|
|
2007
|
+
}
|
|
2008
|
+
})();`;
|
|
2009
|
+
}
|