-
Notifications
You must be signed in to change notification settings - Fork 0
Add Netlify static portfolio demo #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # Netlify Static Portfolio Demo | ||
|
|
||
| This runbook publishes a zero-dollar, always-available portfolio preview of Second Brain as a static Netlify site. It is separate from the normal local-first app: no backend, database, Redis, Gemini key, API token, admin token, or private notes are deployed. | ||
|
|
||
| ## What The Static Demo Shows | ||
|
|
||
| - WattVision web shell, navigation, chat, search, status, sources, feedback, briefing, tasks, research, and admin surfaces. | ||
| - Public-safe fixture corpus matching `python -m app.demo.seed_public`. | ||
| - Deterministic cited answers for regular RAG and Agentic RAG demo prompts. | ||
| - Read-only behavior for capture, ingest, source edits, task creation, research enqueue, eval promotion, deletion, and retention purge. | ||
| - Optional browser-side passcode gate for casual access control. | ||
|
|
||
| ## Important Access Boundary | ||
|
|
||
| `NEXT_PUBLIC_DEMO_ACCESS_HASH` is shipped to the browser because this is a static site. It limits casual visitors but is not a security boundary. Only public-safe content belongs in this demo. | ||
|
|
||
| ## Netlify Site Setup | ||
|
|
||
| 1. Push this branch to GitHub. | ||
| 2. In Netlify, choose **Add new site** -> **Import an existing project**. | ||
| 3. Select the GitHub repository. | ||
| 4. Use the settings already committed in `netlify.toml`: | ||
|
|
||
| ```toml | ||
| [build] | ||
| base = "frontend" | ||
| command = "npm ci && npm run build" | ||
| publish = "out" | ||
| ``` | ||
|
|
||
| 5. Add this environment variable in Netlify: | ||
|
|
||
| ```text | ||
| NEXT_PUBLIC_DEMO_ACCESS_HASH=<lowercase sha256 hash of your demo passcode> | ||
| ``` | ||
|
|
||
| If you omit `NEXT_PUBLIC_DEMO_ACCESS_HASH`, the passcode gate is disabled and all visitors get immediate access. Only add this variable when you want casual access control for the static demo. | ||
|
|
||
| Netlify already receives these from `netlify.toml`: | ||
|
|
||
| ```text | ||
| NEXT_PUBLIC_SECOND_BRAIN_DEMO_MODE=static | ||
| NEXT_PUBLIC_AGENTIC_RAG_ENABLED=true | ||
| NEXT_TELEMETRY_DISABLED=1 | ||
| ``` | ||
|
|
||
| Do not add these to Netlify for the static demo: | ||
|
|
||
| ```text | ||
| DATABASE_URL | ||
| SECOND_BRAIN_API_TOKEN | ||
| SECOND_BRAIN_ADMIN_TOKEN | ||
| GEMINI_API_KEY | ||
| SECOND_BRAIN_TEST_DATABASE_URL | ||
| ``` | ||
|
|
||
| ## Generate The Passcode Hash | ||
|
|
||
| Run this locally in PowerShell. Replace the example passcode before using it. | ||
|
|
||
| ```powershell | ||
| $passcode = "replace-with-your-demo-passcode" | ||
| $bytes = [System.Text.Encoding]::UTF8.GetBytes($passcode) | ||
| $hash = [System.Security.Cryptography.SHA256]::HashData($bytes) | ||
| ($hash | ForEach-Object { $_.ToString("x2") }) -join "" | ||
| ``` | ||
|
|
||
| Paste only the resulting hash into Netlify as `NEXT_PUBLIC_DEMO_ACCESS_HASH`. | ||
|
|
||
| ## Local Verification | ||
|
|
||
| From `frontend/`: | ||
|
|
||
| ```powershell | ||
| npm ci | ||
| npm run lint | ||
| # Verify the normal app build still works before enabling static demo export. | ||
| npm run build | ||
| $env:NEXT_PUBLIC_SECOND_BRAIN_DEMO_MODE="static" | ||
| $env:NEXT_PUBLIC_AGENTIC_RAG_ENABLED="true" | ||
| # Build the Netlify static export. | ||
| npm run build | ||
| ``` | ||
|
|
||
| The static build should create `frontend/out`. Preview it with any static file server, for example: | ||
|
|
||
| ```powershell | ||
| python -m http.server 4173 --directory out | ||
| ``` | ||
|
|
||
| Open `http://localhost:4173/chat/` and verify: | ||
|
|
||
| - `/chat/` answers the suggested prompts with citations. | ||
| - `/search/` returns fixture corpus hits. | ||
| - `/sources/` shows one public demo source and seven documents. | ||
| - `/status/` reports `static-demo` runtime and no MCP mutations. | ||
| - write operations show read-only errors instead of calling a backend. | ||
|
|
||
| ## Portfolio Link | ||
|
|
||
| After Netlify deploys successfully, add the live-demo URL to your personal portfolio site and to `README.md` under a clearly labeled `Live Demo` or `Deployment` section. Keep the repository link immediately adjacent, for example: `Live demo: <Netlify URL> (Repo: <repo URL>)`. Verify the live demo link is reachable after deployment. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,10 @@ | ||
| # Copy to frontend/.env.local for local development. | ||
| NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 | ||
| NEXT_PUBLIC_AGENTIC_RAG_ENABLED=false | ||
|
|
||
| # Static portfolio demo mode. Leave unset for normal local/API-backed use. | ||
| # Netlify should set NEXT_PUBLIC_SECOND_BRAIN_DEMO_MODE=static. | ||
| # Optional: set NEXT_PUBLIC_DEMO_ACCESS_HASH to the lowercase SHA-256 hash of | ||
| # the casual-access passcode. Do not commit the plain passcode. | ||
| NEXT_PUBLIC_SECOND_BRAIN_DEMO_MODE= | ||
| NEXT_PUBLIC_DEMO_ACCESS_HASH= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,31 @@ | ||
| import Link from "next/link"; | ||
| import { redirect } from "next/navigation"; | ||
| import { STATIC_DEMO_MODE } from "@/lib/demo/config"; | ||
|
|
||
| export default function Home() { | ||
| redirect("/chat"); | ||
| if (!STATIC_DEMO_MODE) { | ||
| redirect("/chat"); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex h-full items-center justify-center bg-background px-4 text-foreground"> | ||
| <section className="w-full max-w-md rounded-lg border border-border bg-card p-5 text-center"> | ||
| <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary"> | ||
| Second Brain | ||
| </p> | ||
| <h1 className="mt-2 text-xl font-semibold text-foreground"> | ||
| Open the workspace | ||
| </h1> | ||
| <p className="mt-2 text-sm leading-6 text-muted-foreground"> | ||
| Continue to the chat workspace for cited answers over the public-safe demo corpus. | ||
| </p> | ||
| <Link | ||
| href="/chat" | ||
| className="mt-5 inline-flex h-10 items-center justify-center rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary/25" | ||
| > | ||
| Open chat | ||
| </Link> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| "use client"; | ||
|
|
||
| import type { ReactNode } from "react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { LockKey, ShieldCheck } from "@phosphor-icons/react"; | ||
|
|
||
| import { | ||
| DEMO_ACCESS_ENABLED, | ||
| DEMO_ACCESS_HASH, | ||
| DEMO_ACCESS_STORAGE_KEY, | ||
| STATIC_DEMO_MODE, | ||
| } from "@/lib/demo/config"; | ||
|
|
||
| type AccessState = "checking" | "granted" | "locked"; | ||
|
|
||
| async function sha256Hex(value: string): Promise<string> { | ||
| const bytes = new TextEncoder().encode(value); | ||
| const digest = await window.crypto.subtle.digest("SHA-256", bytes); | ||
| return Array.from(new Uint8Array(digest)) | ||
| .map((byte) => byte.toString(16).padStart(2, "0")) | ||
| .join(""); | ||
| } | ||
|
|
||
| export function DemoAccessGate({ children }: { children: ReactNode }) { | ||
| const [state, setState] = useState<AccessState>( | ||
| STATIC_DEMO_MODE && DEMO_ACCESS_ENABLED ? "checking" : "granted", | ||
| ); | ||
| const [passcode, setPasscode] = useState(""); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (!STATIC_DEMO_MODE || !DEMO_ACCESS_ENABLED) return; | ||
|
|
||
| const saved = window.localStorage.getItem(DEMO_ACCESS_STORAGE_KEY); | ||
| const frame = window.requestAnimationFrame(() => { | ||
| setState(saved === DEMO_ACCESS_HASH ? "granted" : "locked"); | ||
| }); | ||
| return () => window.cancelAnimationFrame(frame); | ||
| }, []); | ||
|
|
||
| const submit = async () => { | ||
| setError(null); | ||
| const normalized = passcode.trim(); | ||
| if (!normalized) { | ||
| setError("Enter the demo passcode."); | ||
| return; | ||
| } | ||
|
|
||
| if (!window.crypto?.subtle) { | ||
| setError("This browser cannot verify the passcode locally."); | ||
| return; | ||
| } | ||
|
|
||
| const hash = await sha256Hex(normalized); | ||
| if (hash === DEMO_ACCESS_HASH) { | ||
| window.localStorage.setItem(DEMO_ACCESS_STORAGE_KEY, hash); | ||
| setState("granted"); | ||
| return; | ||
| } | ||
| setError("Passcode did not match."); | ||
| }; | ||
|
|
||
| if (state === "granted") return <>{children}</>; | ||
|
|
||
| return ( | ||
| <div className="flex h-full w-full items-center justify-center bg-background px-4 py-6 text-foreground"> | ||
| <section className="w-full max-w-md rounded-lg border border-border bg-card p-5 shadow-2xl shadow-black/20"> | ||
| <div className="flex items-start gap-3"> | ||
| <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary ring-1 ring-primary/25"> | ||
| <LockKey size={20} weight="bold" /> | ||
| </div> | ||
| <div className="min-w-0"> | ||
| <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary"> | ||
| Portfolio Demo | ||
| </p> | ||
| <h1 className="mt-1 text-lg font-semibold leading-6 text-foreground"> | ||
| Second Brain static preview | ||
| </h1> | ||
| <p className="mt-2 text-sm leading-6 text-muted-foreground"> | ||
| This read-only Netlify build uses a public-safe corpus and a local passcode check for casual access control. | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <form | ||
| className="mt-5 grid gap-3" | ||
| onSubmit={(event) => { | ||
| event.preventDefault(); | ||
| void submit(); | ||
| }} | ||
| > | ||
| <label className="grid gap-1.5 text-xs font-medium text-muted-foreground"> | ||
| <span>Demo passcode</span> | ||
| <input | ||
| type="password" | ||
| value={passcode} | ||
| onChange={(event) => setPasscode(event.target.value)} | ||
| autoComplete="off" | ||
| className="h-10 rounded-lg border border-input bg-background px-3 text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground hover:bg-surface-hover/60 focus:border-primary focus:ring-3 focus:ring-primary/25" | ||
| placeholder="Enter passcode" | ||
| /> | ||
| </label> | ||
| {error && ( | ||
| <p className="rounded-lg border border-destructive/25 bg-destructive/10 px-3 py-2 text-xs leading-5 text-destructive"> | ||
| {error} | ||
| </p> | ||
| )} | ||
| <button | ||
| type="submit" | ||
| className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-primary px-3 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary/25" | ||
| > | ||
| <ShieldCheck size={15} weight="bold" /> | ||
| Open demo | ||
| </button> | ||
| </form> | ||
|
|
||
| <p className="mt-4 text-[11px] leading-5 text-muted-foreground"> | ||
| The passcode gate is not a security boundary. No private notes, API keys, database URLs, or admin secrets are included in this static build. | ||
| </p> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.