@diegonogueiradev_/mcp-graph 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/api/middleware/error-handler.d.ts +3 -0
- package/dist/api/middleware/error-handler.d.ts.map +1 -0
- package/dist/api/middleware/error-handler.js +35 -0
- package/dist/api/middleware/error-handler.js.map +1 -0
- package/dist/api/middleware/validate.d.ts +5 -0
- package/dist/api/middleware/validate.d.ts.map +1 -0
- package/dist/api/middleware/validate.js +23 -0
- package/dist/api/middleware/validate.js.map +1 -0
- package/dist/api/router.d.ts +11 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +41 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/routes/capture.d.ts +3 -0
- package/dist/api/routes/capture.d.ts.map +1 -0
- package/dist/api/routes/capture.js +31 -0
- package/dist/api/routes/capture.js.map +1 -0
- package/dist/api/routes/context.d.ts +4 -0
- package/dist/api/routes/context.d.ts.map +1 -0
- package/dist/api/routes/context.js +25 -0
- package/dist/api/routes/context.js.map +1 -0
- package/dist/api/routes/docs-cache.d.ts +4 -0
- package/dist/api/routes/docs-cache.d.ts.map +1 -0
- package/dist/api/routes/docs-cache.js +79 -0
- package/dist/api/routes/docs-cache.js.map +1 -0
- package/dist/api/routes/edges.d.ts +4 -0
- package/dist/api/routes/edges.d.ts.map +1 -0
- package/dist/api/routes/edges.js +50 -0
- package/dist/api/routes/edges.js.map +1 -0
- package/dist/api/routes/events.d.ts +4 -0
- package/dist/api/routes/events.d.ts.map +1 -0
- package/dist/api/routes/events.js +37 -0
- package/dist/api/routes/events.js.map +1 -0
- package/dist/api/routes/graph.d.ts +4 -0
- package/dist/api/routes/graph.d.ts.map +1 -0
- package/dist/api/routes/graph.js +39 -0
- package/dist/api/routes/graph.js.map +1 -0
- package/dist/api/routes/import.d.ts +4 -0
- package/dist/api/routes/import.d.ts.map +1 -0
- package/dist/api/routes/import.js +92 -0
- package/dist/api/routes/import.js.map +1 -0
- package/dist/api/routes/insights.d.ts +4 -0
- package/dist/api/routes/insights.d.ts.map +1 -0
- package/dist/api/routes/insights.js +40 -0
- package/dist/api/routes/insights.js.map +1 -0
- package/dist/api/routes/integrations.d.ts +4 -0
- package/dist/api/routes/integrations.d.ts.map +1 -0
- package/dist/api/routes/integrations.js +56 -0
- package/dist/api/routes/integrations.js.map +1 -0
- package/dist/api/routes/nodes.d.ts +4 -0
- package/dist/api/routes/nodes.d.ts.map +1 -0
- package/dist/api/routes/nodes.js +123 -0
- package/dist/api/routes/nodes.js.map +1 -0
- package/dist/api/routes/project.d.ts +4 -0
- package/dist/api/routes/project.d.ts.map +1 -0
- package/dist/api/routes/project.js +33 -0
- package/dist/api/routes/project.js.map +1 -0
- package/dist/api/routes/search.d.ts +4 -0
- package/dist/api/routes/search.d.ts.map +1 -0
- package/dist/api/routes/search.js +25 -0
- package/dist/api/routes/search.js.map +1 -0
- package/dist/api/routes/skills.d.ts +3 -0
- package/dist/api/routes/skills.d.ts.map +1 -0
- package/dist/api/routes/skills.js +16 -0
- package/dist/api/routes/skills.js.map +1 -0
- package/dist/api/routes/stats.d.ts +4 -0
- package/dist/api/routes/stats.d.ts.map +1 -0
- package/dist/api/routes/stats.js +14 -0
- package/dist/api/routes/stats.js.map +1 -0
- package/dist/cli/commands/import-cmd.d.ts +3 -0
- package/dist/cli/commands/import-cmd.d.ts.map +1 -0
- package/dist/cli/commands/import-cmd.js +38 -0
- package/dist/cli/commands/import-cmd.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +55 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +3 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +18 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +3 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +39 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/capture/content-extractor.d.ts +21 -0
- package/dist/core/capture/content-extractor.d.ts.map +1 -0
- package/dist/core/capture/content-extractor.js +74 -0
- package/dist/core/capture/content-extractor.js.map +1 -0
- package/dist/core/capture/web-capture.d.ts +20 -0
- package/dist/core/capture/web-capture.d.ts.map +1 -0
- package/dist/core/capture/web-capture.js +51 -0
- package/dist/core/capture/web-capture.js.map +1 -0
- package/dist/core/config/config-loader.d.ts +3 -0
- package/dist/core/config/config-loader.d.ts.map +1 -0
- package/dist/core/config/config-loader.js +43 -0
- package/dist/core/config/config-loader.js.map +1 -0
- package/dist/core/config/config-schema.d.ts +11 -0
- package/dist/core/config/config-schema.d.ts.map +1 -0
- package/dist/core/config/config-schema.js +12 -0
- package/dist/core/config/config-schema.js.map +1 -0
- package/dist/core/docs/docs-cache-store.d.ts +24 -0
- package/dist/core/docs/docs-cache-store.d.ts.map +1 -0
- package/dist/core/docs/docs-cache-store.js +61 -0
- package/dist/core/docs/docs-cache-store.js.map +1 -0
- package/dist/core/docs/docs-syncer.d.ts +13 -0
- package/dist/core/docs/docs-syncer.d.ts.map +1 -0
- package/dist/core/docs/docs-syncer.js +38 -0
- package/dist/core/docs/docs-syncer.js.map +1 -0
- package/dist/core/events/event-bus.d.ts +26 -0
- package/dist/core/events/event-bus.d.ts.map +1 -0
- package/dist/core/events/event-bus.js +47 -0
- package/dist/core/events/event-bus.js.map +1 -0
- package/dist/core/events/event-types.d.ts +57 -0
- package/dist/core/events/event-types.d.ts.map +1 -0
- package/dist/core/events/event-types.js +2 -0
- package/dist/core/events/event-types.js.map +1 -0
- package/dist/core/graph/mermaid-export.d.ts +9 -0
- package/dist/core/graph/mermaid-export.d.ts.map +1 -0
- package/dist/core/graph/mermaid-export.js +80 -0
- package/dist/core/graph/mermaid-export.js.map +1 -0
- package/dist/core/importer/prd-to-graph.d.ts.map +1 -1
- package/dist/core/importer/prd-to-graph.js +7 -0
- package/dist/core/importer/prd-to-graph.js.map +1 -1
- package/dist/core/insights/bottleneck-detector.d.ts +31 -0
- package/dist/core/insights/bottleneck-detector.d.ts.map +1 -0
- package/dist/core/insights/bottleneck-detector.js +69 -0
- package/dist/core/insights/bottleneck-detector.js.map +1 -0
- package/dist/core/insights/metrics-calculator.d.ts +31 -0
- package/dist/core/insights/metrics-calculator.d.ts.map +1 -0
- package/dist/core/insights/metrics-calculator.js +78 -0
- package/dist/core/insights/metrics-calculator.js.map +1 -0
- package/dist/core/insights/skill-recommender.d.ts +21 -0
- package/dist/core/insights/skill-recommender.d.ts.map +1 -0
- package/dist/core/insights/skill-recommender.js +129 -0
- package/dist/core/insights/skill-recommender.js.map +1 -0
- package/dist/core/integrations/serena-reader.d.ts +18 -0
- package/dist/core/integrations/serena-reader.d.ts.map +1 -0
- package/dist/core/integrations/serena-reader.js +50 -0
- package/dist/core/integrations/serena-reader.js.map +1 -0
- package/dist/core/integrations/tool-status.d.ts +18 -0
- package/dist/core/integrations/tool-status.d.ts.map +1 -0
- package/dist/core/integrations/tool-status.js +92 -0
- package/dist/core/integrations/tool-status.js.map +1 -0
- package/dist/core/parser/file-reader.d.ts +13 -0
- package/dist/core/parser/file-reader.d.ts.map +1 -0
- package/dist/core/parser/file-reader.js +52 -0
- package/dist/core/parser/file-reader.js.map +1 -0
- package/dist/core/parser/read-html.d.ts +7 -0
- package/dist/core/parser/read-html.d.ts.map +1 -0
- package/dist/core/parser/read-html.js +51 -0
- package/dist/core/parser/read-html.js.map +1 -0
- package/dist/core/parser/read-pdf.d.ts +10 -0
- package/dist/core/parser/read-pdf.d.ts.map +1 -0
- package/dist/core/parser/read-pdf.js +16 -0
- package/dist/core/parser/read-pdf.js.map +1 -0
- package/dist/core/planner/next-task.d.ts.map +1 -1
- package/dist/core/planner/next-task.js +4 -1
- package/dist/core/planner/next-task.js.map +1 -1
- package/dist/core/search/fts-search.d.ts.map +1 -1
- package/dist/core/search/fts-search.js +6 -1
- package/dist/core/search/fts-search.js.map +1 -1
- package/dist/core/store/migrations.d.ts.map +1 -1
- package/dist/core/store/migrations.js +38 -0
- package/dist/core/store/migrations.js.map +1 -1
- package/dist/core/store/sqlite-store.d.ts +7 -0
- package/dist/core/store/sqlite-store.d.ts.map +1 -1
- package/dist/core/store/sqlite-store.js +28 -3
- package/dist/core/store/sqlite-store.js.map +1 -1
- package/dist/core/utils/logger.d.ts +1 -0
- package/dist/core/utils/logger.d.ts.map +1 -1
- package/dist/core/utils/logger.js +5 -0
- package/dist/core/utils/logger.js.map +1 -1
- package/dist/mcp/init-project.d.ts.map +1 -1
- package/dist/mcp/init-project.js +12 -16
- package/dist/mcp/init-project.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +17 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.js +0 -0
- package/dist/mcp/tools/export-mermaid.d.ts +4 -0
- package/dist/mcp/tools/export-mermaid.d.ts.map +1 -0
- package/dist/mcp/tools/export-mermaid.js +27 -0
- package/dist/mcp/tools/export-mermaid.js.map +1 -0
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/web/dashboard/dist/assets/code-graph-tab-jvBo8Q9t.js +1 -0
- package/dist/web/dashboard/dist/assets/constants-CLJl-f3f.js +1 -0
- package/dist/web/dashboard/dist/assets/graph-tab-BoKfDlvO.js +1 -0
- package/dist/web/dashboard/dist/assets/graph-utils-BZV40eAE.css +1 -0
- package/dist/web/dashboard/dist/assets/graph-utils-uGOH4eMw.js +23 -0
- package/dist/web/dashboard/dist/assets/index-DM_LGeRr.css +1 -0
- package/dist/web/dashboard/dist/assets/index-DrHbgcp5.js +53 -0
- package/dist/web/dashboard/dist/assets/insights-tab-D7sHV2xV.js +1 -0
- package/dist/web/dashboard/dist/assets/prd-backlog-tab-C_Uq1Qte.js +1 -0
- package/dist/web/dashboard/dist/index.html +13 -0
- package/dist/web/public/css/styles.css +646 -0
- package/dist/web/public/index.html +209 -0
- package/dist/web/public/js/api-client.js +85 -0
- package/dist/web/public/js/app.js +112 -0
- package/dist/web/public/js/capture-form.js +196 -0
- package/dist/web/public/js/filters.js +94 -0
- package/dist/web/public/js/force-graph-renderer.js +498 -0
- package/dist/web/public/js/graph-renderer.js +62 -0
- package/dist/web/public/js/import-form.js +105 -0
- package/dist/web/public/js/node-detail.js +106 -0
- package/dist/web/public/js/tabs/code-graph-tab.js +66 -0
- package/dist/web/public/js/tabs/graph-tab.js +238 -0
- package/dist/web/public/js/tabs/insights-tab.js +236 -0
- package/dist/web/public/js/tabs/knowledge-tab.js +201 -0
- package/dist/web/public/js/tabs/prd-backlog-tab.js +167 -0
- package/dist/web/public/vendor/force-graph.min.js +5 -0
- package/dist/web/public/vendor/mermaid.min.js +2843 -0
- package/package.json +22 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD import form with drag-and-drop
|
|
3
|
+
*/
|
|
4
|
+
const ImportForm = (() => {
|
|
5
|
+
let selectedFile = null;
|
|
6
|
+
|
|
7
|
+
function init() {
|
|
8
|
+
const modal = document.getElementById('import-modal');
|
|
9
|
+
const dropZone = document.getElementById('drop-zone');
|
|
10
|
+
const fileInput = document.getElementById('file-input');
|
|
11
|
+
const submitBtn = document.getElementById('import-submit');
|
|
12
|
+
const cancelBtn = document.getElementById('import-cancel');
|
|
13
|
+
const closeBtn = document.getElementById('modal-close');
|
|
14
|
+
const openBtn = document.getElementById('btn-import-prd');
|
|
15
|
+
|
|
16
|
+
if (!modal || !dropZone || !fileInput) return;
|
|
17
|
+
|
|
18
|
+
openBtn?.addEventListener('click', () => open());
|
|
19
|
+
closeBtn?.addEventListener('click', () => close());
|
|
20
|
+
cancelBtn?.addEventListener('click', () => close());
|
|
21
|
+
|
|
22
|
+
// Drop zone events
|
|
23
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
24
|
+
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
25
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
26
|
+
dropZone.addEventListener('drop', (e) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
dropZone.classList.remove('drag-over');
|
|
29
|
+
if (e.dataTransfer.files.length) selectFile(e.dataTransfer.files[0]);
|
|
30
|
+
});
|
|
31
|
+
fileInput.addEventListener('change', () => {
|
|
32
|
+
if (fileInput.files.length) selectFile(fileInput.files[0]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
submitBtn?.addEventListener('click', () => submit());
|
|
36
|
+
|
|
37
|
+
// Close on backdrop click
|
|
38
|
+
modal.addEventListener('click', (e) => {
|
|
39
|
+
if (e.target === modal) close();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function open() {
|
|
44
|
+
selectedFile = null;
|
|
45
|
+
const modal = document.getElementById('import-modal');
|
|
46
|
+
const status = document.getElementById('import-status');
|
|
47
|
+
const submitBtn = document.getElementById('import-submit');
|
|
48
|
+
const dropZone = document.getElementById('drop-zone');
|
|
49
|
+
|
|
50
|
+
if (status) status.innerHTML = '';
|
|
51
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
52
|
+
if (dropZone) dropZone.querySelector('p').textContent = 'Drag & drop a file here\nor click to select';
|
|
53
|
+
|
|
54
|
+
modal?.showModal();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function close() {
|
|
58
|
+
document.getElementById('import-modal')?.close();
|
|
59
|
+
selectedFile = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function selectFile(file) {
|
|
63
|
+
selectedFile = file;
|
|
64
|
+
const dropZone = document.getElementById('drop-zone');
|
|
65
|
+
const submitBtn = document.getElementById('import-submit');
|
|
66
|
+
|
|
67
|
+
if (dropZone) dropZone.querySelector('p').textContent = `Selected: ${file.name} (${formatSize(file.size)})`;
|
|
68
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function submit() {
|
|
72
|
+
if (!selectedFile) return;
|
|
73
|
+
|
|
74
|
+
const submitBtn = document.getElementById('import-submit');
|
|
75
|
+
const status = document.getElementById('import-status');
|
|
76
|
+
const force = document.getElementById('import-force')?.checked || false;
|
|
77
|
+
|
|
78
|
+
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Importing...'; }
|
|
79
|
+
if (status) status.innerHTML = '<p class="info">Uploading and processing...</p>';
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await ApiClient.importFile(selectedFile, force);
|
|
83
|
+
if (status) {
|
|
84
|
+
status.innerHTML = `<p class="success">Imported ${result.nodesCreated} nodes and ${result.edgesCreated} edges.</p>`;
|
|
85
|
+
}
|
|
86
|
+
// Refresh dashboard
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
close();
|
|
89
|
+
document.dispatchEvent(new CustomEvent('import-completed'));
|
|
90
|
+
}, 1500);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (status) status.innerHTML = `<p class="error">${err.message}</p>`;
|
|
93
|
+
} finally {
|
|
94
|
+
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Import'; }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatSize(bytes) {
|
|
99
|
+
if (bytes < 1024) return bytes + ' B';
|
|
100
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
101
|
+
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { init, open, close };
|
|
105
|
+
})();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node detail sidebar panel
|
|
3
|
+
*/
|
|
4
|
+
const NodeDetail = (() => {
|
|
5
|
+
const STATUS_COLORS = {
|
|
6
|
+
done: '#4caf50', in_progress: '#2196f3', blocked: '#f44336',
|
|
7
|
+
backlog: '#9e9e9e', ready: '#ff9800',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function show(node) {
|
|
11
|
+
const panel = document.getElementById('detail-panel');
|
|
12
|
+
const title = document.getElementById('detail-title');
|
|
13
|
+
const body = document.getElementById('detail-body');
|
|
14
|
+
if (!panel || !body || !title) return;
|
|
15
|
+
|
|
16
|
+
title.textContent = node.title;
|
|
17
|
+
body.innerHTML = buildDetail(node);
|
|
18
|
+
panel.classList.remove('hidden');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hide() {
|
|
22
|
+
const panel = document.getElementById('detail-panel');
|
|
23
|
+
if (panel) panel.classList.add('hidden');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildDetail(node) {
|
|
27
|
+
const color = STATUS_COLORS[node.status] || '#9e9e9e';
|
|
28
|
+
const sections = [];
|
|
29
|
+
|
|
30
|
+
sections.push(`
|
|
31
|
+
<div class="detail-row">
|
|
32
|
+
<span class="detail-label">ID</span>
|
|
33
|
+
<code class="detail-value">${esc(node.id)}</code>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="detail-row">
|
|
36
|
+
<span class="detail-label">Type</span>
|
|
37
|
+
<span class="badge badge-type">${esc(node.type)}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="detail-row">
|
|
40
|
+
<span class="detail-label">Status</span>
|
|
41
|
+
<span class="badge" style="background:${color};color:#fff">${esc(node.status)}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="detail-row">
|
|
44
|
+
<span class="detail-label">Priority</span>
|
|
45
|
+
<span class="detail-value">${node.priority}</span>
|
|
46
|
+
</div>
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
if (node.xpSize) {
|
|
50
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Size</span><span class="detail-value">${esc(node.xpSize)}</span></div>`);
|
|
51
|
+
}
|
|
52
|
+
if (node.estimateMinutes) {
|
|
53
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Estimate</span><span class="detail-value">${node.estimateMinutes}min</span></div>`);
|
|
54
|
+
}
|
|
55
|
+
if (node.sprint) {
|
|
56
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Sprint</span><span class="detail-value">${esc(node.sprint)}</span></div>`);
|
|
57
|
+
}
|
|
58
|
+
if (node.description) {
|
|
59
|
+
sections.push(`<div class="detail-section"><h4>Description</h4><p>${esc(node.description)}</p></div>`);
|
|
60
|
+
}
|
|
61
|
+
if (node.acceptanceCriteria?.length) {
|
|
62
|
+
sections.push(`<div class="detail-section"><h4>Acceptance Criteria</h4><ul>${node.acceptanceCriteria.map(ac => `<li>${esc(ac)}</li>`).join('')}</ul></div>`);
|
|
63
|
+
}
|
|
64
|
+
if (node.tags?.length) {
|
|
65
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Tags</span><span class="detail-value">${node.tags.map(t => `<span class="tag">${esc(t)}</span>`).join(' ')}</span></div>`);
|
|
66
|
+
}
|
|
67
|
+
if (node.sourceRef) {
|
|
68
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Source</span><span class="detail-value">${esc(node.sourceRef.file)}${node.sourceRef.startLine ? ':' + node.sourceRef.startLine : ''}</span></div>`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Created</span><span class="detail-value">${formatDate(node.createdAt)}</span></div>`);
|
|
72
|
+
sections.push(`<div class="detail-row"><span class="detail-label">Updated</span><span class="detail-value">${formatDate(node.updatedAt)}</span></div>`);
|
|
73
|
+
|
|
74
|
+
return sections.join('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function esc(text) {
|
|
78
|
+
if (!text) return '';
|
|
79
|
+
const div = document.createElement('div');
|
|
80
|
+
div.textContent = String(text);
|
|
81
|
+
return div.innerHTML;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatDate(iso) {
|
|
85
|
+
if (!iso) return '-';
|
|
86
|
+
try {
|
|
87
|
+
return new Date(iso).toLocaleString();
|
|
88
|
+
} catch {
|
|
89
|
+
return iso;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function init() {
|
|
94
|
+
document.getElementById('detail-close')?.addEventListener('click', hide);
|
|
95
|
+
document.addEventListener('node-clicked', async (e) => {
|
|
96
|
+
try {
|
|
97
|
+
const node = await ApiClient.getNode(e.detail.nodeId);
|
|
98
|
+
if (node) show(node);
|
|
99
|
+
} catch {
|
|
100
|
+
// Node may not match id format from mermaid
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { init, show, hide };
|
|
106
|
+
})();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab 3: Code Graph — GitNexus iframe + Serena fallback
|
|
3
|
+
*/
|
|
4
|
+
const CodeGraphTab = (() => {
|
|
5
|
+
async function load() {
|
|
6
|
+
const container = document.getElementById('tab-code-graph');
|
|
7
|
+
if (!container) return;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const status = await ApiClient.request('/integrations/status');
|
|
11
|
+
|
|
12
|
+
if (status.gitnexus?.running) {
|
|
13
|
+
container.innerHTML = `
|
|
14
|
+
<div class="iframe-container">
|
|
15
|
+
<div class="iframe-header">
|
|
16
|
+
<span class="badge status-done">GitNexus Running</span>
|
|
17
|
+
<a href="${esc(status.gitnexus.url)}" target="_blank" class="btn btn-sm">Open in new tab</a>
|
|
18
|
+
</div>
|
|
19
|
+
<iframe src="${esc(status.gitnexus.url)}" class="gitnexus-iframe" title="GitNexus Code Graph"></iframe>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Fallback: show Serena memories as static overview
|
|
26
|
+
let html = `
|
|
27
|
+
<div class="fallback-panel">
|
|
28
|
+
<div class="fallback-notice">
|
|
29
|
+
<h3>Code Graph (GitNexus not running)</h3>
|
|
30
|
+
<p>Start GitNexus for interactive code graph visualization:</p>
|
|
31
|
+
<pre><code>gitnexus analyze && gitnexus serve</code></pre>
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
if (status.serena?.memories?.length > 0) {
|
|
36
|
+
html += '<h3 class="section-title">Serena Memories (Codebase Overview)</h3>';
|
|
37
|
+
const memories = await ApiClient.request('/integrations/serena/memories');
|
|
38
|
+
for (const mem of memories) {
|
|
39
|
+
html += `
|
|
40
|
+
<div class="memory-card">
|
|
41
|
+
<h4>${esc(mem.name)}</h4>
|
|
42
|
+
<pre class="memory-content">${esc(mem.content)}</pre>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
html += '<p class="text-muted">No Serena memories found. Configure Serena for codebase intelligence.</p>';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
html += '</div>';
|
|
51
|
+
container.innerHTML = html;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
container.innerHTML = `<div class="error-state"><p>Failed to load: ${err.message}</p></div>`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function esc(text) {
|
|
58
|
+
if (!text) return '';
|
|
59
|
+
const div = document.createElement('div');
|
|
60
|
+
div.textContent = String(text);
|
|
61
|
+
return div.innerHTML;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function init() {}
|
|
65
|
+
return { init, load };
|
|
66
|
+
})();
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab 1: Graph visualization — Force Graph (default) + Mermaid toggle
|
|
3
|
+
*/
|
|
4
|
+
const GraphTab = (() => {
|
|
5
|
+
const VIEW_KEY = 'mcp-graph-view-mode';
|
|
6
|
+
let nodes = [];
|
|
7
|
+
let currentSort = { key: 'priority', dir: 'asc' };
|
|
8
|
+
let viewMode = localStorage.getItem(VIEW_KEY) || 'force';
|
|
9
|
+
|
|
10
|
+
async function load() {
|
|
11
|
+
try {
|
|
12
|
+
nodes = await ApiClient.getNodes();
|
|
13
|
+
if (viewMode === 'force') {
|
|
14
|
+
await renderForceGraph();
|
|
15
|
+
} else {
|
|
16
|
+
await renderMermaid();
|
|
17
|
+
}
|
|
18
|
+
renderTable(nodes);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error('Failed to load graph tab:', err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function renderForceGraph() {
|
|
25
|
+
try {
|
|
26
|
+
const graphData = await ApiClient.getGraph();
|
|
27
|
+
if (!graphData || (!graphData.nodes?.length && !graphData.edges?.length)) {
|
|
28
|
+
const container = document.getElementById('force-graph-container');
|
|
29
|
+
if (container) container.innerHTML = '<div class="empty-state"><p>No graph data. Import a PRD to get started.</p></div>';
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Transform edges → links format for force-graph
|
|
34
|
+
const forceNodes = (graphData.nodes || []).map(n => ({
|
|
35
|
+
id: n.id,
|
|
36
|
+
title: n.title,
|
|
37
|
+
type: n.type,
|
|
38
|
+
status: n.status,
|
|
39
|
+
priority: n.priority,
|
|
40
|
+
description: n.description,
|
|
41
|
+
xpSize: n.xpSize,
|
|
42
|
+
sprint: n.sprint,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const forceLinks = (graphData.edges || []).map(e => ({
|
|
46
|
+
source: e.source,
|
|
47
|
+
target: e.target,
|
|
48
|
+
relationType: e.relationType || e.type,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Apply status/type filters from the filter panel
|
|
52
|
+
const filterVals = Filters.getValues();
|
|
53
|
+
let filteredNodes = forceNodes;
|
|
54
|
+
if (filterVals.status.length) {
|
|
55
|
+
filteredNodes = filteredNodes.filter(n => filterVals.status.includes(n.status));
|
|
56
|
+
}
|
|
57
|
+
if (filterVals.type.length) {
|
|
58
|
+
filteredNodes = filteredNodes.filter(n => filterVals.type.includes(n.type));
|
|
59
|
+
}
|
|
60
|
+
const nodeIds = new Set(filteredNodes.map(n => n.id));
|
|
61
|
+
const filteredLinks = forceLinks.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
62
|
+
|
|
63
|
+
ForceGraphRenderer.render(filteredNodes, filteredLinks);
|
|
64
|
+
|
|
65
|
+
// Apply edge type + depth filters
|
|
66
|
+
const edgeTypes = filterVals.edgeTypes;
|
|
67
|
+
const focusDepth = filterVals.focusDepth;
|
|
68
|
+
ForceGraphRenderer.setFilters({ edgeTypes, depth: focusDepth });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('Failed to render force graph:', err);
|
|
71
|
+
const container = document.getElementById('force-graph-container');
|
|
72
|
+
if (container) container.innerHTML = '<div class="error-state"><p>Error loading graph: ' + (err.message || err) + '</p></div>';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function renderMermaid() {
|
|
77
|
+
const container = document.getElementById('mermaid-output');
|
|
78
|
+
const empty = document.getElementById('graph-empty');
|
|
79
|
+
if (!container) return;
|
|
80
|
+
|
|
81
|
+
const params = Filters.toQueryParams();
|
|
82
|
+
const mermaidCode = await ApiClient.getMermaid(params);
|
|
83
|
+
|
|
84
|
+
if (!mermaidCode || mermaidCode.trim() === 'graph TD\n' || mermaidCode.trim() === 'graph TD') {
|
|
85
|
+
if (empty) empty.classList.remove('hidden');
|
|
86
|
+
container.innerHTML = '';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (empty) empty.classList.add('hidden');
|
|
91
|
+
await GraphRenderer.render(container, mermaidCode);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function renderGraph() {
|
|
95
|
+
if (viewMode === 'force') {
|
|
96
|
+
await renderForceGraph();
|
|
97
|
+
} else {
|
|
98
|
+
await renderMermaid();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setViewMode(mode) {
|
|
103
|
+
viewMode = mode;
|
|
104
|
+
localStorage.setItem(VIEW_KEY, mode);
|
|
105
|
+
|
|
106
|
+
// Toggle active button
|
|
107
|
+
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
108
|
+
btn.classList.toggle('active', btn.dataset.view === mode);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Toggle containers
|
|
112
|
+
const forceContainer = document.getElementById('force-graph-container');
|
|
113
|
+
const mermaidContainer = document.getElementById('mermaid-container');
|
|
114
|
+
const zoomControls = document.getElementById('zoom-controls');
|
|
115
|
+
|
|
116
|
+
if (forceContainer) forceContainer.classList.toggle('hidden', mode !== 'force');
|
|
117
|
+
if (mermaidContainer) mermaidContainer.classList.toggle('hidden', mode !== 'mermaid');
|
|
118
|
+
if (zoomControls) zoomControls.style.display = mode === 'force' ? 'flex' : 'none';
|
|
119
|
+
|
|
120
|
+
// Toggle filter visibility
|
|
121
|
+
Filters.setViewMode(mode);
|
|
122
|
+
|
|
123
|
+
// Render the active view
|
|
124
|
+
renderGraph();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderTable(data) {
|
|
128
|
+
const tbody = document.getElementById('node-table-body');
|
|
129
|
+
if (!tbody) return;
|
|
130
|
+
|
|
131
|
+
if (data.length === 0) {
|
|
132
|
+
tbody.innerHTML = '<tr><td colspan="6" class="empty-cell">No nodes found</td></tr>';
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
tbody.innerHTML = data.map(node => `
|
|
137
|
+
<tr class="node-row" data-id="${esc(node.id)}">
|
|
138
|
+
<td class="cell-title">${esc(node.title)}</td>
|
|
139
|
+
<td><span class="badge badge-type">${esc(node.type)}</span></td>
|
|
140
|
+
<td><span class="badge status-${node.status}">${esc(node.status)}</span></td>
|
|
141
|
+
<td class="cell-center">${node.priority}</td>
|
|
142
|
+
<td class="cell-center">${node.xpSize || '-'}</td>
|
|
143
|
+
<td>${esc(node.sprint || '-')}</td>
|
|
144
|
+
</tr>
|
|
145
|
+
`).join('');
|
|
146
|
+
|
|
147
|
+
// Row click → detail
|
|
148
|
+
tbody.querySelectorAll('.node-row').forEach(row => {
|
|
149
|
+
row.addEventListener('click', () => {
|
|
150
|
+
const nodeId = row.dataset.id;
|
|
151
|
+
document.dispatchEvent(new CustomEvent('node-clicked', { detail: { nodeId } }));
|
|
152
|
+
// Also select in force graph
|
|
153
|
+
if (viewMode === 'force') {
|
|
154
|
+
ForceGraphRenderer.selectNode(nodeId);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sortTable(key) {
|
|
161
|
+
if (currentSort.key === key) {
|
|
162
|
+
currentSort.dir = currentSort.dir === 'asc' ? 'desc' : 'asc';
|
|
163
|
+
} else {
|
|
164
|
+
currentSort = { key, dir: 'asc' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
168
|
+
const va = a[key] ?? '';
|
|
169
|
+
const vb = b[key] ?? '';
|
|
170
|
+
const cmp = typeof va === 'number' ? va - vb : String(va).localeCompare(String(vb));
|
|
171
|
+
return currentSort.dir === 'asc' ? cmp : -cmp;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
renderTable(sorted);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function filterTable(query) {
|
|
178
|
+
if (!query) { renderTable(nodes); return; }
|
|
179
|
+
const q = query.toLowerCase();
|
|
180
|
+
const filtered = nodes.filter(n =>
|
|
181
|
+
n.title.toLowerCase().includes(q) ||
|
|
182
|
+
n.type.toLowerCase().includes(q) ||
|
|
183
|
+
n.status.toLowerCase().includes(q) ||
|
|
184
|
+
(n.sprint || '').toLowerCase().includes(q)
|
|
185
|
+
);
|
|
186
|
+
renderTable(filtered);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function init() {
|
|
190
|
+
// Initialize force graph renderer
|
|
191
|
+
ForceGraphRenderer.init('force-graph-container');
|
|
192
|
+
|
|
193
|
+
// View toggle buttons
|
|
194
|
+
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
195
|
+
btn.addEventListener('click', () => setViewMode(btn.dataset.view));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Zoom controls
|
|
199
|
+
document.getElementById('zoom-in')?.addEventListener('click', () => ForceGraphRenderer.zoomIn());
|
|
200
|
+
document.getElementById('zoom-out')?.addEventListener('click', () => ForceGraphRenderer.zoomOut());
|
|
201
|
+
document.getElementById('zoom-fit')?.addEventListener('click', () => ForceGraphRenderer.zoomFit());
|
|
202
|
+
|
|
203
|
+
// Filter buttons
|
|
204
|
+
document.getElementById('btn-apply-filters')?.addEventListener('click', renderGraph);
|
|
205
|
+
document.getElementById('btn-clear-filters')?.addEventListener('click', () => {
|
|
206
|
+
Filters.clear();
|
|
207
|
+
renderGraph();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Table sort
|
|
211
|
+
document.querySelectorAll('#node-table th[data-sort]').forEach(th => {
|
|
212
|
+
th.style.cursor = 'pointer';
|
|
213
|
+
th.addEventListener('click', () => sortTable(th.dataset.sort));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Table search
|
|
217
|
+
document.getElementById('table-search')?.addEventListener('input', (e) => {
|
|
218
|
+
filterTable(e.target.value);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Resize handler
|
|
222
|
+
window.addEventListener('resize', () => {
|
|
223
|
+
if (viewMode === 'force') ForceGraphRenderer.resize();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Apply initial view mode
|
|
227
|
+
setViewMode(viewMode);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function esc(text) {
|
|
231
|
+
if (!text) return '';
|
|
232
|
+
const div = document.createElement('div');
|
|
233
|
+
div.textContent = String(text);
|
|
234
|
+
return div.innerHTML;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { init, load, renderGraph };
|
|
238
|
+
})();
|