From 5d742c17479e3d142d3f0a692609b7ebfef2296d Mon Sep 17 00:00:00 2001
From: azizu06
Date: Sun, 12 Jul 2026 15:08:44 -0400
Subject: [PATCH] Preserve CapCheck as a read-only portfolio demo
---
.env.example | 6 ++++
README.md | 36 ++++++++++++++-----
src/app/analyze/page.tsx | 10 ++++++
src/app/globals.css | 22 ++++++++++++
src/app/page.tsx | 9 ++++-
src/components/portfolio-demo-notice.test.tsx | 19 ++++++++++
src/components/portfolio-demo-notice.tsx | 32 +++++++++++++++++
src/lib/portfolio-mode.test.ts | 20 +++++++++++
src/lib/portfolio-mode.ts | 11 ++++++
9 files changed, 156 insertions(+), 9 deletions(-)
create mode 100644 src/components/portfolio-demo-notice.test.tsx
create mode 100644 src/components/portfolio-demo-notice.tsx
create mode 100644 src/lib/portfolio-mode.test.ts
create mode 100644 src/lib/portfolio-mode.ts
diff --git a/.env.example b/.env.example
index 84ec646..9f8f095 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,12 @@
CAPCHECK_ANALYSIS_MODE=fixture
+CAPCHECK_FEED_MODE=fixture
# Live mode is selected when CAPCHECK_ANALYSIS_MODE is not "fixture".
# GEMINI_API_KEY=
# FINNHUB_KEY=
# Server-only key for Verified Feed refresh (YouTube Data API v3 discovery).
# YOUTUBE_API_KEY=
+# Supabase-backed feed configuration. The anon key is public and remains
+# protected by Row Level Security; never expose the server-only secret key.
+# NEXT_PUBLIC_SUPABASE_URL=
+# NEXT_PUBLIC_SUPABASE_ANON_KEY=
+# SUPABASE_SERVICE_ROLE_KEY=
diff --git a/README.md b/README.md
index a16c1e5..2d6d0ec 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,25 @@ scorecard. A user can paste a video URL or upload a video, follow the analysis
stages, and inspect the evidence behind each supported, contradicted, or
unverifiable claim.
-This repository contains the Bloomberg Hackathon 2026 application. Local and
-browser tests use deterministic fixtures, while live mode streams the real
-analysis pipelines through the same runtime-validated contracts.
+Built for BloomKnights 2026, CapCheck combines Gemini video understanding,
+Google Search grounding, Finnhub market data, and a Supabase-backed Verified
+Feed.
+
+> **Project status:** Complete and preserved as a read-only portfolio showcase.
+> Browse the deployed app at [capcheck-sigma.vercel.app](https://capcheck-sigma.vercel.app/).
+> The persisted feed and evidence pages remain live; paid/private analysis and
+> refresh integrations were retired after the hackathon.
+
+## What shipped
+
+- URL and upload intake with streamed analysis progress.
+- Gemini claim extraction, grounded verification, and deterministic Cap Scores.
+- Finnhub function calling for quantitative market claims.
+- A searchable, category-filtered catalog of CapCheck-vetted YouTube videos.
+- Supabase persistence, idempotent refresh runs, reliability gating, and safe
+ failure behavior.
+- Responsive, accessible feed, detail, and scorecard experiences backed by
+ deterministic unit and Playwright coverage.
## Local setup
@@ -71,11 +87,10 @@ The shared seam is the Zod-validated `AnalysisEvent` contract in
completion, and error events that a live adapter must produce. The UI consumes
only the SSE response from `/api/analyze`; it does not import either adapter.
-Lane A can introduce the live yt-dlp, Gemini, and evidence-grounding adapter by
-selecting it on the server while preserving the route and `AnalysisEvent`
-stream. Lane B can continue working against fixtures. Contract changes require
-a coordinated update to schemas, fixtures, adapter tests, parser tests, and UI
-tests before either lane depends on them.
+The production implementation selects the live yt-dlp, Gemini, Search
+grounding, and Finnhub adapters while preserving the same `AnalysisEvent`
+stream used by deterministic fixtures. Contract changes require coordinated
+updates to schemas, fixtures, adapter tests, parser tests, and UI tests.
The live claim-extraction entry point is
`createNodeClaimExtractionPipeline` in
@@ -163,6 +178,11 @@ duplicate) with no credentials. Live mode additionally requires a server-only
`YOUTUBE_API_KEY` (plus `GEMINI_API_KEY`, `FINNHUB_KEY`, and
`SUPABASE_SERVICE_ROLE_KEY`).
+The hosted portfolio deployment intentionally omits those private credentials.
+It reads the persisted catalog through the public Supabase anon key under RLS,
+while live refresh and analysis render an archived-demo state instead of
+calling external providers.
+
Opt-in live smoke test (never part of CI):
```bash
diff --git a/src/app/analyze/page.tsx b/src/app/analyze/page.tsx
index e565754..2712b35 100644
--- a/src/app/analyze/page.tsx
+++ b/src/app/analyze/page.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { CapCheckApp } from "@/components/capcheck-app";
+import { PortfolioDemoNotice } from "@/components/portfolio-demo-notice";
+import { isPortfolioDemoMode } from "@/lib/portfolio-mode";
export const metadata: Metadata = {
title: "Analyze — CapCheck",
@@ -8,5 +10,13 @@ export const metadata: Metadata = {
};
export default function AnalyzePage() {
+ if (isPortfolioDemoMode()) {
+ return (
+
+
+
+ );
+ }
+
return ;
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 34dfd4b..fe6d938 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1453,6 +1453,28 @@ input:disabled {
font-size: 0.92rem;
}
+.portfolio-demo-notice {
+ margin-top: 24px;
+}
+
+.portfolio-demo-notice > svg {
+ color: var(--ink);
+}
+
+.portfolio-demo-notice .kicker {
+ margin-bottom: 6px;
+}
+
+.portfolio-demo-notice h2 {
+ margin: 0 0 6px;
+ color: var(--ink);
+ font: 600 clamp(1.2rem, 2.5vw, 1.6rem)/1.2 var(--display);
+}
+
+.portfolio-demo-page {
+ max-width: 760px;
+}
+
.feed-state-action {
display: inline-flex;
align-items: center;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7994825..e5e4da0 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,8 +2,10 @@ import { TriangleAlert } from "lucide-react";
import Link from "next/link";
import { FeedExplorer } from "@/components/feed/feed-explorer";
+import { PortfolioDemoNotice } from "@/components/portfolio-demo-notice";
import { RefreshFeedButton } from "@/components/refresh-feed-button";
import type { CatalogItem } from "@/domain/feed";
+import { isPortfolioDemoMode } from "@/lib/portfolio-mode";
import { getCatalogRepository } from "@/server/feed/catalog-repository";
export const dynamic = "force-dynamic";
@@ -15,6 +17,7 @@ export default async function FeedHome({
}) {
let items: CatalogItem[] = [];
let failed = false;
+ const portfolioDemo = isPortfolioDemoMode();
const { feedState } = await searchParams;
const fixtureState =
process.env.NODE_ENV !== "production" &&
@@ -55,7 +58,11 @@ export default async function FeedHome({
only if its claims held up. Open one to see the Cap Score, the
evidence, and the citations behind it.
-
+ {portfolioDemo ? (
+
+ ) : (
+
+ )}
{failed ? (
diff --git a/src/components/portfolio-demo-notice.test.tsx b/src/components/portfolio-demo-notice.test.tsx
new file mode 100644
index 0000000..74d7493
--- /dev/null
+++ b/src/components/portfolio-demo-notice.test.tsx
@@ -0,0 +1,19 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { PortfolioDemoNotice } from "./portfolio-demo-notice";
+
+describe("PortfolioDemoNotice", () => {
+ it("keeps the archived project useful without offering retired live actions", () => {
+ render();
+
+ expect(
+ screen.getByRole("heading", { name: "Live analysis is retired" }),
+ ).toBeVisible();
+ expect(screen.getByRole("link", { name: "Browse the verified feed" })).toHaveAttribute(
+ "href",
+ "/",
+ );
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/portfolio-demo-notice.tsx b/src/components/portfolio-demo-notice.tsx
new file mode 100644
index 0000000..d012baf
--- /dev/null
+++ b/src/components/portfolio-demo-notice.tsx
@@ -0,0 +1,32 @@
+import { Archive } from "lucide-react";
+import Link from "next/link";
+
+export function PortfolioDemoNotice({
+ feature,
+}: {
+ feature: "analyzer" | "refresh";
+}) {
+ const analyzer = feature === "analyzer";
+
+ return (
+
+
+
+
Archived portfolio demo
+
+ {analyzer ? "Live analysis is retired" : "The catalog is now read-only"}
+
+
+ {analyzer
+ ? "The hackathon API integrations have been safely deactivated. The verified catalog, scorecards, citations, and video details remain available to explore."
+ : "Live refresh has been safely deactivated after the hackathon. Browse the persisted CapCheck-vetted videos and their evidence-backed scorecards below."}
+