Skip to content

Commit b7a8f97

Browse files
cdeustclaude
andcommitted
revert(viz): restore Graph view to pre-d3-removal state
Restores the original force-graph rendering (d3.forceSimulation + phase-driven append loader + unconditional /api/graph fetch on boot) that produced the working ~340k-node neural-cloud screenshot. The performance toast, graph-warn banner, EXPERIMENTAL chip, and view-gated lazy fetches are all rolled back. Files reverted to 4a41aff: ui/unified-viz.html ui/unified/js/workflow_graph.js ui/unified/js/controls.js ui/unified/js/polling.js ui/unified/js/state.js Files KEPT from 63bacca: mcp_server/handlers/memories_page.py (paged /api/memories) mcp_server/handlers/memories_facets.py (aggregate counts) mcp_server/server/http_standalone.py (route additions) ui/unified/js/knowledge.js (lazy-load + filter chips) ui/unified/js/timeline.js (lazy-load + filter chips) ui/unified/timeline.css (always-visible Kanban cards) Graph + Pipeline now work as they did before today's session, while Knowledge + Board retain the paged endpoint + server-side filters (domain / stage / emotion / hot / protected / search / sort) so they stay responsive at 150k+ memories. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 63bacca commit b7a8f97

5 files changed

Lines changed: 47 additions & 210 deletions

File tree

ui/unified-viz.html

Lines changed: 13 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -26,57 +26,6 @@
2626
<div class="loader-sub">Initializing neural map</div>
2727
</div>
2828

29-
<!-- Performance-notice toast. Banner at the top of the page explaining
30-
why Graph + Pipeline are disabled and that Wiki is still draft.
31-
Auto-dismissable via the close button. -->
32-
<div id="cortex-perf-toast" role="status"
33-
style="position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:5000;
34-
max-width:780px;padding:10px 44px 10px 18px;
35-
background:linear-gradient(90deg,rgba(20,28,42,0.95),rgba(28,36,52,0.95));
36-
border:1px solid rgba(224,176,64,0.45);border-radius:6px;
37-
color:#E8E4D8;font:12px/1.5 -apple-system,Inter,system-ui,sans-serif;
38-
letter-spacing:0.2px;box-shadow:0 6px 22px rgba(0,0,0,0.45);
39-
backdrop-filter:blur(6px)">
40-
<div style="display:flex;align-items:flex-start;gap:10px">
41-
<div style="color:#E0B040;font-size:14px;line-height:1;margin-top:1px"></div>
42-
<div>
43-
<div style="color:#E0B040;font-weight:600;margin-bottom:3px;letter-spacing:0.6px;font-size:11px;text-transform:uppercase">
44-
Performance notice
45-
</div>
46-
<div style="color:#c4d4dc">
47-
<strong>Graph</strong> and <strong>Pipeline</strong> views are temporarily
48-
deactivated due to performance issues at scale. They will return once we
49-
solve the rendering bottleneck. <strong>Knowledge</strong> and
50-
<strong>Board</strong> are the recommended views — both lazy-load and
51-
scale to any number of memories. <strong>Wiki</strong> remains visible
52-
but is still <em>under construction</em> — not the final version.
53-
</div>
54-
</div>
55-
</div>
56-
<button id="cortex-perf-toast-close" aria-label="Dismiss notice"
57-
style="position:absolute;top:6px;right:8px;width:24px;height:24px;
58-
background:transparent;border:none;color:#7a8e9c;font-size:16px;
59-
cursor:pointer;line-height:1;padding:0">×</button>
60-
</div>
61-
<script>
62-
(function(){
63-
var t = document.getElementById('cortex-perf-toast');
64-
var b = document.getElementById('cortex-perf-toast-close');
65-
if (!t || !b) return;
66-
// Restore the dismissed state from localStorage so the toast doesn't
67-
// re-appear on every reload once the user has acknowledged it.
68-
try {
69-
if (localStorage.getItem('cortexPerfToastDismissed') === '1') {
70-
t.style.display = 'none';
71-
}
72-
} catch (_) {}
73-
b.addEventListener('click', function(){
74-
t.style.display = 'none';
75-
try { localStorage.setItem('cortexPerfToastDismissed', '1'); } catch (_) {}
76-
});
77-
})();
78-
</script>
79-
8029
<!-- Non-blocking progress bar wired to /api/graph/progress. Shows
8130
the server's layered build status (L0 domains → L1 … L6 AST)
8231
without ever taking the graph away from the user. Fades in when
@@ -90,10 +39,12 @@
9039
<div style="text-align:right;margin-top:4px;color:#7a8e9c;font-size:10px;font-variant-numeric:tabular-nums"><span id="bp-pct">0%</span><span id="bp-elapsed" style="color:#5a6e7c;margin-left:6px"></span></div>
9140
</div>
9241
<script>
93-
// Phase-driven append-only loader. Active ONLY when the Graph tab
94-
// is the current view — switching away from Graph stops the polling
95-
// loop so its cost (each phase append re-runs buildGraph) doesn't
96-
// hit Knowledge / Board users.
42+
// Phase-driven append-only loader.
43+
// Polls /api/graph/progress; when a phase flips to `ready`, fetches
44+
// /api/graph/phase?name=<key> ONCE and appends its nodes+edges to the
45+
// live scene via JUG.appendGraphDelta. Never re-fetches the whole
46+
// graph — matches the user's requirement: each phase pops new nodes
47+
// when the previous one is done.
9748
(function(){
9849
// Phases are now DYNAMIC: L0..L5 are fixed, then one phase per
9950
// project (L6:<name>) then L6_CROSS. We discover the full set from
@@ -146,19 +97,13 @@
14697
if(p.elapsed) document.getElementById('bp-elapsed').textContent = ' · '+p.elapsed.toFixed(1)+'s';
14798
}
14899

149-
function _activeViewIsGraph() {
150-
return window.JUG && JUG.state && JUG.state.activeView === 'graph';
151-
}
152100
function poll(){
153-
// Only do graph-build work while the user is on the Graph tab.
154-
// Knowledge / Board / Wiki users don't pay the cost.
155-
if (!_activeViewIsGraph()) {
156-
setTimeout(poll, 1500);
157-
return;
158-
}
159101
fetch('/api/graph/progress').then(function(r){ return r.ok ? r.json() : null; }).then(function(p){
160102
if(p){
161103
refreshBar(p);
104+
// Sequential append: iterate the server's phases dict in
105+
// insertion order (L0..L5, then L6:<proj1>..<projN>, then
106+
// L6_CROSS). Apply each one whose `ready` flag is true.
162107
var seq = Promise.resolve();
163108
var keys = Object.keys(p.phases || {});
164109
keys.forEach(function(k){
@@ -221,18 +166,11 @@
221166

222167
<div id="filter-bar">
223168
<span class="view-toggle">
224-
<!-- Graph re-enabled but flagged with a warning banner below
225-
(#cortex-graph-warn) shown when the user activates it.
226-
Knowledge stays the default landing tab. Pipeline still off
227-
pending its own paged rewrite. Wiki keeps the DRAFT chip. -->
228-
<button class="view-btn" data-view="graph"
229-
title="Graph view — currently unstable at scale; see the warning below">Graph <span style="font-size:9px;color:#E0B040;letter-spacing:0.6px;margin-left:4px">EXPERIMENTAL</span></button>
230-
<button class="view-btn active" data-view="knowledge">Knowledge</button>
231-
<button class="view-btn" data-view="wiki" title="Wiki — under construction; not the final version">Wiki <span style="font-size:9px;color:#E0B040;letter-spacing:0.6px;margin-left:4px">DRAFT</span></button>
169+
<button class="view-btn active" data-view="graph">Graph</button>
170+
<button class="view-btn" data-view="knowledge">Knowledge</button>
171+
<button class="view-btn" data-view="wiki">Wiki</button>
232172
<button class="view-btn" data-view="timeline">Board</button>
233-
<button class="view-btn" data-view="sankey" disabled
234-
title="Pipeline view temporarily disabled — performance issue at scale"
235-
style="opacity:0.35;cursor:not-allowed">Pipeline</button>
173+
<button class="view-btn" data-view="sankey">Pipeline</button>
236174
</span>
237175
<button class="filter-btn" id="glossary-toggle" title="Glossary (?)">?</button>
238176
<span class="filter-sep"></span>
@@ -281,39 +219,6 @@
281219
<input type="text" id="search-box" placeholder="Search path, skill, command…">
282220
</div>
283221

284-
<!-- Graph-only warning banner. Shown directly below the view-toggle
285-
row when the Graph tab is active. The Graph view is enabled but
286-
can be unresponsive at scale — explain the situation in-line so
287-
users aren't confused if their browser hangs. Hidden by default;
288-
controls.js toggles display on view change. -->
289-
<div id="cortex-graph-warn" role="status"
290-
style="display:none;position:fixed;top:62px;left:50%;transform:translateX(-50%);z-index:200;
291-
max-width:760px;padding:8px 36px 8px 14px;
292-
background:linear-gradient(90deg,rgba(48,28,12,0.92),rgba(64,40,16,0.92));
293-
border:1px solid rgba(224,140,64,0.5);border-radius:5px;
294-
color:#FFE3B5;font:11.5px/1.45 -apple-system,Inter,system-ui,sans-serif;
295-
letter-spacing:0.2px;box-shadow:0 4px 14px rgba(0,0,0,0.4)">
296-
<span style="color:#FFB060;margin-right:6px"></span>
297-
<strong>Graph may be unusable at this scale.</strong>
298-
Due to performance issues we are currently investigating, opening the Graph
299-
view can freeze the browser when memory counts are high. We are actively
300-
working on it. Use <strong>Knowledge</strong> or <strong>Board</strong> for
301-
reliable browsing while we land the rewrite.
302-
<button id="cortex-graph-warn-close" aria-label="Dismiss"
303-
style="position:absolute;top:4px;right:6px;width:22px;height:22px;
304-
background:transparent;border:none;color:#FFB060;font-size:14px;
305-
cursor:pointer;line-height:1;padding:0">×</button>
306-
</div>
307-
<script>
308-
(function(){
309-
var b = document.getElementById('cortex-graph-warn-close');
310-
if (b) b.addEventListener('click', function() {
311-
var w = document.getElementById('cortex-graph-warn');
312-
if (w) w.style.display = 'none';
313-
});
314-
})();
315-
</script>
316-
317222
<div id="status-bar" class="hud-panel">
318223
<div class="status-dot"></div>
319224
<span id="status-text">Initializing</span>

ui/unified/js/controls.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,23 @@
33
document.addEventListener('DOMContentLoaded', function() {
44
// ── View toggle (Graph / Timeline) ──
55
var viewBtns = document.querySelectorAll('.view-toggle .view-btn[data-view]');
6-
function _toggleGraphWarn(view) {
7-
var w = document.getElementById('cortex-graph-warn');
8-
if (!w) return;
9-
// Show the experimental-graph banner only when the Graph tab is
10-
// active. Other views hide it; the in-banner × button hides it
11-
// permanently for the session.
12-
if (view === 'graph') w.style.display = '';
13-
else w.style.display = 'none';
14-
}
156
viewBtns.forEach(function(btn) {
167
btn.addEventListener('click', function() {
17-
// Disabled tabs (Pipeline) ignore clicks entirely.
18-
if (btn.disabled || btn.hasAttribute('disabled')) return;
198
viewBtns.forEach(function(b) { b.classList.remove('active'); });
209
btn.classList.add('active');
21-
var view = btn.dataset.view || 'knowledge';
10+
var view = btn.dataset.view || 'graph';
2211
JUG.state.activeView = view;
2312
toggleFilterBarVisibility(view);
24-
_toggleGraphWarn(view);
2513
});
2614
});
2715

28-
// Graph disabled. Knowledge is the landing tab — paged
29-
// /api/memories scales to any N.
16+
// Graph is the default landing view — the wiki is still under
17+
// active restructuring and the workflow graph is the canonical
18+
// answer to "what did Claude do in this project".
3019
setTimeout(function() {
3120
JUG.state.activeView = '_init';
32-
JUG.state.activeView = 'knowledge';
33-
toggleFilterBarVisibility('knowledge');
21+
JUG.state.activeView = 'graph';
22+
toggleFilterBarVisibility('graph');
3423
}, 0);
3524

3625
// ── Filter buttons (source type) ──

ui/unified/js/polling.js

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,11 @@
33
var abortController = null;
44

55
function fetchGraph() {
6-
// Graph re-enabled (experimental). Knowledge is still the default
7-
// landing tab so this fetch only matters once the user clicks the
8-
// Graph tab — the warning banner above the view tells them what
9-
// they're getting into. d3-force is gone (workflow_graph.js
10-
// mounts a static-position renderer) but JSON.parse + buildGraph
11-
// are still O(N+E) each and may freeze the browser at high N.
12-
if (JUG.state && JUG.state.activeView !== 'graph') {
13-
// Don't pay the cost unless the user actually wants the graph.
14-
updateStatus('Online — graph standby');
15-
hideLoading();
16-
return;
17-
}
186
if (abortController) abortController.abort();
197
abortController = new AbortController();
208
var signal = abortController.signal;
9+
10+
// Single fetch — no batching. Domain dedup keeps node count manageable.
2111
fetch(JUG.API_URL, { signal: signal })
2212
.then(function(res) {
2313
if (!res.ok) throw new Error('HTTP ' + res.status);
@@ -144,9 +134,7 @@
144134
.map(function(v) { return String(v).padStart(2, '0'); }).join(':');
145135
}, 1000);
146136

147-
// Boot — delay initial fetch. fetchGraph() short-circuits when the
148-
// Graph tab isn't active, so this is cheap on Knowledge / Board /
149-
// Wiki landings.
137+
// Boot — delay initial fetch to let Three.js scene fully initialize
150138
if (document.readyState === 'loading') {
151139
document.addEventListener('DOMContentLoaded', function() {
152140
setTimeout(fetchGraph, 500);
@@ -155,19 +143,8 @@
155143
setTimeout(fetchGraph, 500);
156144
}
157145

158-
// Trigger the heavy graph fetch only when the user actually
159-
// switches to the Graph tab (lazy-load semantics).
160-
if (window.JUG && JUG.on) {
161-
JUG.on('state:activeView', function(ev) {
162-
if (ev && ev.value === 'graph') setTimeout(fetchGraph, 50);
163-
});
164-
}
165-
166146
function _loadDiscussionBatch(batch) {
167-
// Discussion batches were merged into JUG.state.lastData — at high
168-
// N this also accumulates hundreds of MB. Skip until we add a
169-
// dedicated Discussions tab with its own paged path.
170-
return;
147+
var batchSize = 500;
171148
fetch(JUG.API_URL.replace('/api/graph', '/api/discussions') + '?batch=' + batch + '&batch_size=' + batchSize)
172149
.then(function(r) { return r.json(); })
173150
.then(function(data) {

ui/unified/js/state.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
selectedId: null,
1818
zoomLevel: 'L0',
1919
lastData: null,
20-
activeView: 'knowledge',
20+
activeView: 'graph',
2121
domainFilter: '',
2222
emotionFilter: '',
2323
stageFilter: '',

ui/unified/js/workflow_graph.js

Lines changed: 23 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -249,61 +249,28 @@
249249
// * velocityDecay: 0.72 → 0.78 (ζ recovered to ~0.65)
250250
// Other force constants unchanged — slots from computeSlots carry
251251
// the positioning burden; physics just needs time to converge.
252-
// ── d3-force simulation REMOVED ─────────────────────────────────
253-
//
254-
// The simulation was the dominant cost driver at high N: per tick
255-
// O(N log N) charge + O(E) link + O(N) collide + O(N) center
256-
// = ~5M ops/tick × 60 fps = 300M ops/sec on the main thread for a
257-
// 240k-node graph. Plus warmup 100 ticks + cooldown 400 ticks per
258-
// mount. That's what froze the browser.
259-
//
260-
// Positions are now FINAL once `prepareTopology` has placed each
261-
// node into its slot (slotOf[id]). The renderer paints once at
262-
// those positions and stops — no tick loop, no continuous physics.
263-
// Drag handlers move the node directly and call `renderer.refresh()`
264-
// for a single repaint.
265-
//
266-
// We expose a `sim` shim so the existing renderers
267-
// (workflow_graph_render_canvas.js / _svg.js) — which call
268-
// `sim.on('tick', draw)`, `sim.alphaTarget(...).restart()` etc.
269-
// for drag handling — keep working without modification.
270-
nodes.forEach(function (n) {
271-
var slot = ctx.slotOf && ctx.slotOf[n.id];
272-
if (slot) { n.x = slot.x; n.y = slot.y; }
273-
else if (n.x == null) { n.x = ctx.cx || width / 2; n.y = ctx.cy || height / 2; }
274-
});
275-
var _tickCb = function () {};
276-
var sim = {
277-
_nodes: nodes,
278-
_scheduled: false,
279-
nodes: function (n) { if (n) { this._nodes = n; } return this._nodes; },
280-
force: function () { return sim; }, // no-op chainable
281-
alpha: function () { return sim; }, // no-op chainable
282-
alphaDecay: function () { return sim; },
283-
alphaTarget: function () { return sim; },
284-
velocityDecay: function () { return sim; },
285-
restart: function () {
286-
// Coalesce repaints to one per animation frame even under
287-
// drag-storm — without this the SVG renderer redraws ~120
288-
// times/sec while the user drags one node.
289-
if (sim._scheduled) return sim;
290-
sim._scheduled = true;
291-
requestAnimationFrame(function () {
292-
sim._scheduled = false;
293-
try { _tickCb(); } catch (_) {}
294-
});
295-
return sim;
296-
},
297-
stop: function () { return sim; },
298-
on: function (ev, cb) {
299-
if (ev === 'tick' && typeof cb === 'function') {
300-
_tickCb = cb;
301-
// Single initial paint at the static slot positions.
302-
try { cb(); } catch (_) {}
303-
}
304-
return sim;
305-
},
306-
};
252+
var slotK = HEAVY ? 1.2 : 0.85;
253+
var chargeEn = true;
254+
var collideI = HEAVY ? 2 : 3;
255+
var alphaDK = HEAVY ? 0.018 : 0.022;
256+
var velDecay = 0.78;
257+
258+
var sim = d3.forceSimulation(nodes)
259+
.alpha(1.0).alphaDecay(alphaDK).velocityDecay(velDecay)
260+
.force('link', d3.forceLink(edges).id(function (n) { return n.id; })
261+
.distance(linkDistance).strength(linkStrength))
262+
.force('slot', slotForce(ctx, slotK))
263+
.force('interdomain', interDomainRepelForce(ctx, 0.08))
264+
.force('symmulti', symbolMultiCenterForce(ctx))
265+
.force('collide', d3.forceCollide()
266+
.radius(function (n) { return collisionRadius(n, ctx); })
267+
.strength(0.92).iterations(collideI));
268+
if (chargeEn) {
269+
// Local charge (distanceMax 180) so symbol-symbol repulsion
270+
// doesn't create long-range feedback with the multi-centroid
271+
// attraction; domains still repel each other via interdomain.
272+
sim.force('charge', d3.forceManyBody().strength(chargeStrength).distanceMax(180));
273+
}
307274

308275
var useCanvas = nodes.length > CANVAS_THRESHOLD;
309276
var renderer = useCanvas
@@ -314,8 +281,7 @@
314281
var w = container.clientWidth || window.innerWidth;
315282
var h = container.clientHeight || window.innerHeight;
316283
renderer.resize(w, h);
317-
// No simulation to restart — just repaint at the new viewport.
318-
sim.restart();
284+
sim.alpha(0.3).restart();
319285
}
320286
window.addEventListener('resize', onResize);
321287

0 commit comments

Comments
 (0)