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