From 685aec8d11ebb5bca72c57311a1ae4c33ca43ac1 Mon Sep 17 00:00:00 2001
From: tomsmith8 <142233216+tomsmith8@users.noreply.github.com>
Date: Mon, 9 Nov 2026 10:46:52 +0000
Subject: [PATCH 1/2] Show graph statistics (total nodes, CVE count) on the
homepage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
I added a stats row to the homepage feed showing total nodes, plus a CVE count only when the graph has CVEs. Nothing was run or type-checked, so the code and the new tests are unverified.
- **Helper and tests:** `src/lib/graph-stats.ts` has `getDisplayStats` and `formatStatNumber`. `getDisplayStats` always returns "Nodes" from `node_count` and returns nothing if `node_count` isn't a number. It adds a type stat only when its count is a number above 0. The per-type list is a `TYPE_STATS` array, currently just `cve_count` → "CVEs", so other graphs can add entries later. `src/lib/__tests__/graph-stats.test.ts` covers the cases you listed.
- **Component:** `src/components/feed/graph-stats.tsx` calls `getStats` once on mount with an `AbortController` that aborts on unmount. It shows a pulse skeleton while loading, a muted "Stats unavailable" line on error (with `console.error`), and the grouped numbers on success. It renders nothing if `node_count` is missing. It has no admin or sign-in gate, so anonymous visitors of a public graph see it.
- **Mock mode:** when mocks are enabled it skips the API call and shows a fixed "Nodes 1,234" tile.
- **Wiring:** `feed-view.tsx` renders the component above `HotTakes`, only when there is no search term. That is the landing view, so it hides during search results; say if you want it always visible.
- **Interface change:** in `src/lib/graph-api.ts` I exported `StatsResponse` and added optional `node_count` and `cve_count`.
I did not write a component test; only the helper is covered.
---
src/components/feed/feed-view.tsx | 2 +
src/components/feed/graph-stats.tsx | 58 +++++++++++++++++++++++++++
src/lib/__tests__/graph-stats.test.ts | 25 ++++++++++++
src/lib/graph-api.ts | 4 +-
src/lib/graph-stats.ts | 26 ++++++++++++
5 files changed, 114 insertions(+), 1 deletion(-)
create mode 100644 src/components/feed/graph-stats.tsx
create mode 100644 src/lib/__tests__/graph-stats.test.ts
create mode 100644 src/lib/graph-stats.ts
diff --git a/src/components/feed/feed-view.tsx b/src/components/feed/feed-view.tsx
index b923d6e..8420f3c 100644
--- a/src/components/feed/feed-view.tsx
+++ b/src/components/feed/feed-view.tsx
@@ -11,6 +11,7 @@ import { getLatestNodes } from "@/lib/graph-api"
import { schemaTypeComparator } from "@/lib/node-schema-utils"
import { FeedCard } from "./feed-card"
import { HotTakes } from "./hot-takes"
+import { GraphStats } from "./graph-stats"
import { cn } from "@/lib/utils"
export function FeedView() {
@@ -91,6 +92,7 @@ export function FeedView() {
return (
+ {showHotTakes &&
}
{showHotTakes &&
}
{(showHotTakes || typeCounts.length > 1) && (
diff --git a/src/components/feed/graph-stats.tsx b/src/components/feed/graph-stats.tsx
new file mode 100644
index 0000000..ee1be68
--- /dev/null
+++ b/src/components/feed/graph-stats.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { getStats } from "@/lib/graph-api"
+import { isMocksEnabled } from "@/lib/mock-data"
+import { formatStatNumber, getDisplayStats, type GraphStat } from "@/lib/graph-stats"
+
+const MOCK_STATS: GraphStat[] = [{ label: "Nodes", value: 1234 }]
+
+export function GraphStats() {
+ const mocks = isMocksEnabled()
+ const [stats, setStats] = useState
(mocks ? MOCK_STATS : null)
+ const [error, setError] = useState(false)
+
+ useEffect(() => {
+ if (mocks) return
+ const controller = new AbortController()
+ getStats(controller.signal)
+ .then((res) => setStats(getDisplayStats(res)))
+ .catch((err) => {
+ if (controller.signal.aborted) return
+ console.error("[graph-stats] getStats failed:", err)
+ setError(true)
+ })
+ return () => controller.abort()
+ }, [mocks])
+
+ if (error) {
+ return (
+
+ Stats unavailable
+
+ )
+ }
+
+ if (stats === null) {
+ return (
+
+ )
+ }
+
+ if (stats.length === 0) return null
+
+ return (
+
+ {stats.map((s) => (
+
+
+ {s.label}
+
+
{formatStatNumber(s.value)}
+
+ ))}
+
+ )
+}
diff --git a/src/lib/__tests__/graph-stats.test.ts b/src/lib/__tests__/graph-stats.test.ts
new file mode 100644
index 0000000..7ee925d
--- /dev/null
+++ b/src/lib/__tests__/graph-stats.test.ts
@@ -0,0 +1,25 @@
+import { describe, it, expect } from "vitest"
+import { getDisplayStats, formatStatNumber } from "../graph-stats"
+
+describe("getDisplayStats", () => {
+ it("returns nodes and CVEs when cve_count present", () => {
+ expect(getDisplayStats({ node_count: 10, cve_count: 3 })).toEqual([
+ { label: "Nodes", value: 10 },
+ { label: "CVEs", value: 3 },
+ ])
+ })
+ it("returns only nodes when cve_count absent or 0", () => {
+ expect(getDisplayStats({ node_count: 10 })).toEqual([{ label: "Nodes", value: 10 }])
+ expect(getDisplayStats({ node_count: 10, cve_count: 0 })).toEqual([{ label: "Nodes", value: 10 }])
+ })
+ it("uses node_count, not num_nodes", () => {
+ expect(getDisplayStats({ node_count: 5, num_nodes: 0 })[0].value).toBe(5)
+ })
+ it("returns nothing without node_count", () => {
+ expect(getDisplayStats({ num_nodes: 7 })).toEqual([])
+ expect(getDisplayStats(null)).toEqual([])
+ })
+ it("formats with grouping", () => {
+ expect(formatStatNumber(1776798)).toBe("1,776,798")
+ })
+})
diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts
index 88abd8c..1b0525c 100644
--- a/src/lib/graph-api.ts
+++ b/src/lib/graph-api.ts
@@ -33,7 +33,9 @@ interface NodesListResponse {
totalCount?: number
}
-interface StatsResponse {
+export interface StatsResponse {
+ node_count?: number
+ cve_count?: number
num_nodes: number
num_episodes: number
num_people: number
diff --git a/src/lib/graph-stats.ts b/src/lib/graph-stats.ts
new file mode 100644
index 0000000..b76e166
--- /dev/null
+++ b/src/lib/graph-stats.ts
@@ -0,0 +1,26 @@
+import type { StatsResponse } from "@/lib/graph-api"
+
+export interface GraphStat {
+ label: string
+ value: number
+}
+
+// Per-type counts shown only when the graph reports a positive count.
+// Add entries here to surface more node types.
+export const TYPE_STATS: readonly { key: string; label: string }[] = [
+ { key: "cve_count", label: "CVEs" },
+]
+
+export function getDisplayStats(stats: Partial | null | undefined): GraphStat[] {
+ if (!stats || typeof stats.node_count !== "number") return []
+ const result: GraphStat[] = [{ label: "Nodes", value: stats.node_count }]
+ for (const { key, label } of TYPE_STATS) {
+ const value = (stats as Record)[key]
+ if (typeof value === "number" && value > 0) result.push({ label, value })
+ }
+ return result
+}
+
+export function formatStatNumber(n: number): string {
+ return n.toLocaleString("en-US")
+}
From 0bbeaa2e5399230c23c05060a7a3eb5fc0a56e95 Mon Sep 17 00:00:00 2001
From: tomsmith8 <142233216+tomsmith8@users.noreply.github.com>
Date: Mon, 9 Nov 2026 10:47:29 +0000
Subject: [PATCH 2/2] Show graph statistics (total nodes, CVE count) on the
homepage
In `src/lib/graph-api.ts`, `StatsResponse` now has `node_count: number` (required) and no `cve_count` line. The index signature covers `cve_count`. I didn't run a type-check or build, so the TS2411 fix is unconfirmed.
I re-read `src/lib/graph-stats.ts` and `src/lib/__tests__/graph-stats.test.ts` and found nothing that needs changing:
- **`getDisplayStats`:** it takes `Partial`, so `node_count` is `number | undefined` and the `typeof` check still narrows it.
- **`cve_count`:** it is read through the `Record` cast, so it doesn't depend on the removed property.
- **Test literals:** the ones I looked at, such as `{ node_count: 5, num_nodes: 0 }` and `{ num_nodes: 7 }`, are valid `Partial` values. I only grepped the test file for `num_nodes` and `StatsResponse`, so I didn't read every literal in it, including the `cve_count: 3` one.
Nothing else in `src` builds a full `StatsResponse` literal that would now be missing the required `node_count`.
---
src/lib/graph-api.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts
index 1b0525c..f434a69 100644
--- a/src/lib/graph-api.ts
+++ b/src/lib/graph-api.ts
@@ -34,8 +34,7 @@ interface NodesListResponse {
}
export interface StatsResponse {
- node_count?: number
- cve_count?: number
+ node_count: number
num_nodes: number
num_episodes: number
num_people: number