Skip to content

Commit 1ef2b1b

Browse files
aksOpsclaude
andcommitted
Fix UI dashboard, topology connections, H2 dedup, Neo4j serve, ResultSet leaks
Dashboard: - Fix [object Object] display for connections, infra, auth sections - Render nested API response properly (graph.nodes, frameworks map, etc.) - Add architecture bar chart, connections breakdown, infra sub-sections - Fix FrameworkBadges to handle Record<string,number> (not string[]) Topology: - Add 6 missing edge kinds to RUNTIME_EDGES: PUBLISHES, LISTENS, SENDS_TO, RECEIVES_FROM, INVOKES_RMI, EXPORTS_RMI AnalysisCache: - Fix loadAllNodes() dedup: use MAX(row_id) instead of MIN(data) to keep most complete node version (was losing framework properties) - Fix 10 ResultSet leaks: wrap all executeQuery() in try-with-resources Serve: - Enable Neo4j in serving profile (was false, breaking graph API) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 84ee8ad commit 1ef2b1b

9 files changed

Lines changed: 635 additions & 204 deletions

File tree

src/main/frontend/src/components/Dashboard.tsx

Lines changed: 290 additions & 152 deletions
Large diffs are not rendered by default.

src/main/frontend/src/components/FrameworkBadges.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,33 @@ const frameworkColors: Record<string, string> = {
3131
const defaultColor = 'bg-surface-700/30 text-surface-300 border-surface-600/30';
3232

3333
interface FrameworkBadgesProps {
34-
frameworks: string[];
34+
/** Map of framework name -> count of nodes using that framework */
35+
frameworks: Record<string, number>;
3536
}
3637

3738
export default function FrameworkBadges({ frameworks }: FrameworkBadgesProps) {
38-
if (!frameworks || frameworks.length === 0) return null;
39+
const entries = Object.entries(frameworks || {});
40+
if (entries.length === 0) return null;
41+
42+
// Sort by count descending
43+
const sorted = entries.sort(([, a], [, b]) => b - a);
3944

4045
return (
4146
<div className="glass-card p-5">
4247
<h3 className="text-xs font-medium text-surface-400 uppercase tracking-wider mb-3">
4348
Frameworks & Technologies
4449
</h3>
4550
<div className="flex flex-wrap gap-2">
46-
{frameworks.map(fw => {
51+
{sorted.map(([fw, count]) => {
4752
const lower = fw.toLowerCase();
4853
const color = Object.entries(frameworkColors).find(([k]) => lower.includes(k))?.[1] || defaultColor;
4954
return (
5055
<span
5156
key={fw}
52-
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium border ${color} transition-all hover:scale-105`}
57+
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium border ${color} transition-all hover:scale-105`}
5358
>
5459
{fw}
60+
<span className="opacity-60 font-mono">{count.toLocaleString()}</span>
5561
</span>
5662
);
5763
})}

src/main/frontend/src/types/api.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
1-
export interface StatsResponse {
2-
total_nodes: number;
3-
total_edges: number;
4-
total_files: number;
1+
// Matches StatsService.computeStats() output (primary, from H2 cache)
2+
export interface ComputedStatsResponse {
3+
graph: { nodes: number; edges: number; files: number };
54
languages: Record<string, number>;
6-
node_kinds: Record<string, number>;
7-
edge_kinds: Record<string, number>;
8-
layers: Record<string, number>;
9-
frameworks?: string[];
10-
infrastructure?: Record<string, number>;
11-
auth?: Record<string, number>;
12-
connections?: Record<string, number>;
13-
[key: string]: unknown;
5+
frameworks: Record<string, number>;
6+
infra: {
7+
databases: Record<string, number>;
8+
messaging: Record<string, number>;
9+
cloud: Record<string, number>;
10+
};
11+
connections: {
12+
rest: { total: number; by_method: Record<string, number> };
13+
grpc: number;
14+
websocket: number;
15+
producers: number;
16+
consumers: number;
17+
};
18+
auth: Record<string, number>;
19+
architecture: Record<string, number>;
20+
}
21+
22+
// Matches QueryService.getStats() output (fallback, from Neo4j)
23+
export interface QueryStatsResponse {
24+
node_count: number;
25+
edge_count: number;
26+
nodes_by_kind: Record<string, number>;
27+
nodes_by_layer: Record<string, number>;
28+
}
29+
30+
// Union type -- the /api/stats endpoint may return either format
31+
export type StatsResponse = ComputedStatsResponse | QueryStatsResponse;
32+
33+
// Type guard
34+
export function isComputedStats(s: StatsResponse): s is ComputedStatsResponse {
35+
return 'graph' in s;
1436
}
1537

1638
export interface DetailedStatsResponse {
17-
architecture?: CategoryStats;
18-
frameworks?: CategoryStats;
19-
infrastructure?: CategoryStats;
20-
auth?: CategoryStats;
21-
connections?: CategoryStats;
39+
architecture?: Record<string, unknown>;
40+
frameworks?: Record<string, unknown>;
41+
infrastructure?: Record<string, unknown>;
42+
auth?: Record<string, unknown>;
43+
connections?: Record<string, unknown>;
2244
[key: string]: unknown;
2345
}
2446

src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,10 @@ private void initDb() throws SQLException {
124124
public synchronized String getLastCommit() {
125125
try (var stmt = conn.prepareStatement(
126126
"SELECT commit_sha FROM analysis_runs ORDER BY timestamp DESC LIMIT 1")) {
127-
ResultSet rs = stmt.executeQuery();
128-
if (rs.next()) {
129-
return rs.getString(1);
127+
try (ResultSet rs = stmt.executeQuery()) {
128+
if (rs.next()) {
129+
return rs.getString(1);
130+
}
130131
}
131132
} catch (SQLException e) {
132133
log.debug("Failed to get last commit", e);
@@ -143,7 +144,9 @@ public synchronized boolean isCached(String contentHash) {
143144
try (var stmt = conn.prepareStatement(
144145
"SELECT 1 FROM files WHERE content_hash = ?")) {
145146
stmt.setString(1, contentHash);
146-
return stmt.executeQuery().next();
147+
try (ResultSet rs = stmt.executeQuery()) {
148+
return rs.next();
149+
}
147150
} catch (SQLException e) {
148151
log.debug("Cache lookup failed", e);
149152
return false;
@@ -235,18 +238,20 @@ public synchronized CachedResult loadCachedResults(String contentHash) {
235238
List<CodeNode> nodes = new ArrayList<>();
236239
try (var stmt = conn.prepareStatement("SELECT data FROM nodes WHERE content_hash = ?")) {
237240
stmt.setString(1, contentHash);
238-
ResultSet rs = stmt.executeQuery();
239-
while (rs.next()) {
240-
nodes.add(deserializeNode(rs.getString(1)));
241+
try (ResultSet rs = stmt.executeQuery()) {
242+
while (rs.next()) {
243+
nodes.add(deserializeNode(rs.getString(1)));
244+
}
241245
}
242246
}
243247

244248
List<CodeEdge> edges = new ArrayList<>();
245249
try (var stmt = conn.prepareStatement("SELECT data FROM edges WHERE content_hash = ?")) {
246250
stmt.setString(1, contentHash);
247-
ResultSet rs = stmt.executeQuery();
248-
while (rs.next()) {
249-
edges.add(deserializeEdge(rs.getString(1)));
251+
try (ResultSet rs = stmt.executeQuery()) {
252+
while (rs.next()) {
253+
edges.add(deserializeEdge(rs.getString(1)));
254+
}
250255
}
251256
}
252257

@@ -457,24 +462,24 @@ private CodeEdge deserializeEdge(String json) {
457462
}
458463

459464
private long countFiles() throws SQLException {
460-
try (var stmt = conn.createStatement()) {
461-
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM files");
465+
try (var stmt = conn.createStatement();
466+
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM files")) {
462467
rs.next();
463468
return rs.getLong(1);
464469
}
465470
}
466471

467472
private long countEdges() throws SQLException {
468-
try (var stmt = conn.createStatement()) {
469-
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM edges");
473+
try (var stmt = conn.createStatement();
474+
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM edges")) {
470475
rs.next();
471476
return rs.getLong(1);
472477
}
473478
}
474479

475480
private long countAnalysisRuns() throws SQLException {
476-
try (var stmt = conn.createStatement()) {
477-
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM analysis_runs");
481+
try (var stmt = conn.createStatement();
482+
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM analysis_runs")) {
478483
rs.next();
479484
return rs.getLong(1);
480485
}
@@ -484,8 +489,8 @@ private long countAnalysisRuns() throws SQLException {
484489
* Return the total number of cached nodes.
485490
*/
486491
public synchronized long getNodeCount() {
487-
try (var stmt = conn.createStatement()) {
488-
ResultSet rs = stmt.executeQuery("SELECT COUNT(DISTINCT id) FROM nodes");
492+
try (var stmt = conn.createStatement();
493+
ResultSet rs = stmt.executeQuery("SELECT COUNT(DISTINCT id) FROM nodes")) {
489494
rs.next();
490495
return rs.getLong(1);
491496
} catch (SQLException e) {
@@ -529,11 +534,16 @@ public void storeBatchResults(String batchId, String filePath, String language,
529534
*/
530535
public synchronized List<CodeNode> loadAllNodes() {
531536
List<CodeNode> nodes = new ArrayList<>();
532-
// Use MIN(data) with GROUP BY id to deduplicate nodes that appear in multiple files
533-
try (var stmt = conn.prepareStatement("SELECT MIN(data) FROM nodes GROUP BY id")) {
534-
ResultSet rs = stmt.executeQuery();
535-
while (rs.next()) {
536-
nodes.add(deserializeNode(rs.getString(1)));
537+
// Deduplicate by id, keeping the LAST inserted version (most complete data)
538+
try (var stmt = conn.prepareStatement("""
539+
SELECT n.data FROM nodes n
540+
INNER JOIN (SELECT id, MAX(row_id) AS max_id FROM nodes GROUP BY id) m
541+
ON n.id = m.id AND n.row_id = m.max_id
542+
""")) {
543+
try (ResultSet rs = stmt.executeQuery()) {
544+
while (rs.next()) {
545+
nodes.add(deserializeNode(rs.getString(1)));
546+
}
537547
}
538548
} catch (SQLException e) {
539549
log.debug("Failed to load all nodes", e);
@@ -549,9 +559,10 @@ public synchronized List<CodeNode> loadAllNodes() {
549559
public synchronized List<CodeEdge> loadAllEdges() {
550560
List<CodeEdge> edges = new ArrayList<>();
551561
try (var stmt = conn.prepareStatement("SELECT data FROM edges")) {
552-
ResultSet rs = stmt.executeQuery();
553-
while (rs.next()) {
554-
edges.add(deserializeEdge(rs.getString(1)));
562+
try (ResultSet rs = stmt.executeQuery()) {
563+
while (rs.next()) {
564+
edges.add(deserializeEdge(rs.getString(1)));
565+
}
555566
}
556567
} catch (SQLException e) {
557568
log.debug("Failed to load all edges", e);

src/main/java/io/github/randomcodespace/iq/query/TopologyService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
/**
2424
* Service topology analysis — works on in-memory node/edge lists.
2525
* <p>
26-
* Runtime connections only: CALLS, PRODUCES, CONSUMES, QUERIES, CONNECTS_TO.
26+
* Runtime connections: CALLS, PRODUCES, CONSUMES, QUERIES, CONNECTS_TO,
27+
* PUBLISHES, LISTENS, SENDS_TO, RECEIVES_FROM, INVOKES_RMI, EXPORTS_RMI.
2728
* DEPENDS_ON is excluded (build dependency, not runtime).
2829
*/
2930
@Service
@@ -35,7 +36,13 @@ public class TopologyService {
3536
EdgeKind.PRODUCES,
3637
EdgeKind.CONSUMES,
3738
EdgeKind.QUERIES,
38-
EdgeKind.CONNECTS_TO
39+
EdgeKind.CONNECTS_TO,
40+
EdgeKind.PUBLISHES,
41+
EdgeKind.LISTENS,
42+
EdgeKind.SENDS_TO,
43+
EdgeKind.RECEIVES_FROM,
44+
EdgeKind.INVOKES_RMI,
45+
EdgeKind.EXPORTS_RMI
3946
);
4047

4148
/**

src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ spring:
5858

5959
codeiq:
6060
neo4j:
61-
enabled: false
61+
enabled: true

0 commit comments

Comments
 (0)