feat: Nevermined Tutorials showcase website - #65
Conversation
A Next.js (App Router) website that turns the tutorials repo into a visual, navigable showcase. Every non-deprecated tutorial is presented in one normalized shape — what you'll learn, how it works, under the hood, and a "see it run" panel — behind a persistent left sidebar grouped by protocol (Catalog / x402 HTTP / MCP / LangChain). - Content-driven: one typed array (content/tutorials.ts) renders all pages. - Live "see it run": real x402-shaped handshake (402 -> authorize -> settle) with a per-session credit balance, served by a local sandbox agent (lib/demo-agent.mjs, unit-tested) — no external service, no real money. - Recap tier: the two catalog demos embed video + on-chain receipt; Song From the Headlines also links Rod's alternative take. - Light, docs-palette design (official nevermined.ai/docs colors), syntax- highlighted code, per-file GitHub links, protocol-agnostic copy. - Dockerfile (Next.js standalone, non-root) + .dockerignore for ArgoCD/k8s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UxQXzKhM37FPvPuHUnKHh
|
👀 Reviewing |
GitHub Actions workflow that builds showcase/Dockerfile and pushes to europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-showcase (keyless WIF auth, matching the other Nevermined repos). Runs on pushes to main touching showcase/**, and on manual dispatch. Versioning: immutable sha-<short> per build (pin this in ArgoCD for prod), latest on main, and an optional semver via the dispatch `version` input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UxQXzKhM37FPvPuHUnKHh
|
Added the image CI (option A): Versioning: immutable Two infra checks before this can push on merge (both are GCP-side, not code):
It triggers on push to |
r-marques
left a comment
There was a problem hiding this comment.
🤖 Automated PR review — 🟡 Mergeable with nits
A new Next.js showcase site for the tutorials repo (33 files, +4,356/−0, all additions). The content layer holds up well under checking, and the sandbox is honest about what it is — my main concern going in was that a "live payment sandbox" on a payments company's site might read as real money, and it does not: the panel says so in the UI, not just in a code comment.
dbe2e47c → 536c49fe), so I discarded the first pass and re-ran against the new head rather than posting a review of code that had already changed. The delta added .github/workflows/showcase-image.yml and a README section — which lands directly on my main finding, so it is reviewed below in its current form, not its original one.
What I reviewed
- Diff range
origin/main...HEAD— 33 files, +4,356/−0, head536c49fe. - Panel: none — stood on direct verification.
Verified
- The sandbox does not misrepresent itself.
LiveRunPanel.tsx:234renders "Sandbox agent — real payment round-trips and a real per-session credit balance, no real money" to the visitor, andapp/api/agent/route.ts:7-11says the same to the next developer. It speaks the real x402 shape (402 → authorize → 200 + settlement) and calls no external service. That is the right way round. - Every per-file GitHub link resolves.
repoFileUrlbuilds<repo>/blob/main/<repoPath>/<filePath>, so I checked them all against the tree: 20/20 file links across 8 tutorials resolve, and all 10repoPathdirectories exist. No dead links — the classic defect for a generated docs surface. - The Docker build is coherent.
next.config.mjssetsoutput: "standalone", which is what makesCOPY .next/standalonevalid; the two recap.mp4s the build pulls fromcatalog/are present and git-tracked (19.8 MB + 15.4 MB); and the root.dockerignoredoes not excludecatalog/, so that COPY resolves. The runner stage createsnextjs:nodejsand setsUSER nextjs, so the non-root claim holds. - The new workflow's auth and tagging shape are right — keyless WIF with no stored secret, and an immutable
sha-<short>per build that the summary and README both tell you to pin in ArgoCD. That matches the direction argocd#604 is moving in. - No secrets. Grepped the new
app/,components/,lib/andcontent/for keys, tokens and bearer literals — every hit is documentation prose about tutorial env vars, plus thenvm_democookie name. - The gitignored videos are a documented step, not an omission:
showcase/.gitignoreexcludespublic/media/**/*.mp4and points atnpm run sync:media.
Verdict: 🟡 Mergeable with nits — 0 blockers, 3 should-fix, 1 nit.
🧭 Inline comments (4)
Each is posted on its line in Files changed.
- 🟡 SHOULD FIX — No
pull_requesttrigger — the firstnext buildruns after merge, inside the publish job —.github/workflows/showcase-image.yml:12 - 🟡 SHOULD FIX — Pushing
latestcontradicts theimmutableTags=truepremise argocd#604 relies on for this same registry —.github/workflows/showcase-image.yml:52 - 🟡 SHOULD FIX — The "make it genuinely live" instruction sits five lines above a client-controlled balance —
showcase/app/api/agent/route.ts:10 - 💡 NIT — Excludes
node_modulesbut not Python build artifacts, in a repo that is half Python tutorials —.dockerignore:3
r-marques
left a comment
There was a problem hiding this comment.
🟡 Approved with comments — no blockers; 3 should-fix, 1 nit(s) left inline.
Inline review: #65 (review)
Addresses r-marques's review on #65 (3 should-fix + 1 nit): - ci: add a `pull_request` trigger + a credential-free `verify` job (npm ci · node lib/demo-agent.mjs · next build). `next build` is the type-check, so a broken content/tutorials.ts now fails the PR instead of merging and breaking `main`. `build-push` gets `needs: verify` and `if: github.event_name != 'pull_request'` — publishes only on main/dispatch. - ci: drop the moving `latest` tag. The AR repo has immutableTags=true (verified), so the second default-branch push would be rejected and could fail the whole tag set. `sha-<short>` (immutable) is what ArgoCD pins. - route.ts: warn the next developer that the nvm_demo cookie is client-supplied and unsigned — must move authorized/balance server-side before wiring a real spending backend. - .dockerignore: exclude Python venvs/bytecode (repo is half Poetry tutorials); drop the duplicate `.git` line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UxQXzKhM37FPvPuHUnKHh
eruizgar91
left a comment
There was a problem hiding this comment.
Code review
A brand-new Next.js 15 App Router site under showcase/, so the usual repo invariants barely apply — I weighted the review toward the simulated payment handshake, the Docker/standalone wiring, App Router correctness, and whether the hand-maintained catalogue points at things that actually exist. Five findings are posted inline (1 high, 3 medium, 1 low); the highest one is that the sandbox session cookie is not scoped per tutorial, which retires the flagship 402 demo after the visitor's first authorize.
Checked and came back clean:
- Dockerfile / standalone wiring — three-stage
deps/build/runnermatches the official pattern,output: "standalone"is set,PORT/HOSTNAMEare honoured byserver.js, the image runs as the non-rootnextjsuser, and.next/static+publicare both copied. EveryCOPYsource resolves at this head, including the two gitignored.mp4s pulled fromcatalog/;npm ciruns beforeNODE_ENV=production, sotypescriptis present fornext build. The repo-root.dockerignoreexcludes nothing the build needs. package-lock.json— lockfileVersion 3, 62 entries, every one carryingresolved, and all fivedevDependenciespresent, sonpm ciwill not fail the lockfile/manifest consistency check.- App Router — Next 15's
paramsis correctlyawaited in bothgenerateMetadataand the page component,generateStaticParamsis present,notFound()is used properly, and only serializable data crosses the server→client boundary. CodeBlock/dangerouslySetInnerHTML— no XSS path. The highlighted string comes solely from the static samples incontent/tutorials.ts, never from/api/agentresponses or the chat input, andPrism.highlightescapes its input before emitting token spans.- Catalogue links — all 10
repoPathvalues and all 19tech.files[].pathentries resolve to real paths at this head, including across thehttp-simple-agent→http-simple-agent-tsrename and theweather-mcpsrc/server/main.ts→src/main.tsmove. None of the six tutorials deleted in #64 are referenced. - Secrets / binaries — nothing credential-shaped committed;
song.mp3andalbum-cover.jpgare the deliberate demo assets the README documents.
Considered and deliberately discarded:
- The session cookie is unsigned and client-forgeable. True, but nothing of value is behind it — the balance is simulated and no real money moves — so forging it only lets a visitor cheat their own demo. Not worth a signing scheme here.
- README says both recap demos show "playable outputs, the on-chain receipt";
diligence-in-a-boxhas neither (RecapPanelrenders both conditionally, so nothing breaks). Real, but a documentation-parity nitpick rather than a defect. showcase/is not added to the rootREADME.mdorCLAUDE.mdrepository-structure tree. Worth doing, but the one documentation rule in root CLAUDE.md is scoped to modifying tutorials, andshowcase/is a site, not a tutorial — no specific convention to cite.npmhere vsyarn/pnpmin the sibling tutorials. The repo already mixes yarn and pnpm, so a third is not a new violation.- Two
// ponytail:authoring markers left in committed source (content/tutorials.ts,scripts/sync-media.sh) — cosmetic, no functional impact. - No CI covered
showcase/— addressed by theci: build & push showcase imagecommit that landed mid-review.
Note: the head moved from dbe2e47 to 536c49f while this was running. That commit only adds .github/workflows/showcase-image.yml and a README section, and touches none of the files below, so every finding was re-verified against 536c49f.
…panel copy Addresses eruizgar91's review on #65 (1 high, 3 medium, 1 low): - HIGH: key the sandbox session per tutorial slug. The nvm_demo cookie was a single global {authorized,balance}, so authorizing on one live tutorial made every other tutorial's `ask` skip the 402 (the whole "see it run" handshake played once per visitor) and inherit a foreign balance across agents that charge different credits. route.ts now stores a { [slug]: {authorized,balance} } map — each tutorial gets its own handshake and its own 100 credits. respond() was already per-substate, so no logic change there. Old single-object cookies are detected by their top-level `balance` and dropped. - MEDIUM: subtitles now render. HTML5 <track> accepts WebVTT only; content pointed at .srt, so both recap videos showed no captions despite advertising "EN/ES subtitles". sync-media.sh derives .vtt from each .srt (WEBVTT header + comma→dot timestamps), the four .vtt are committed, and both EN and ES tracks are wired (RecapPanel maps a track list; type is now an array). - MEDIUM: rewrite 7 live-panel notes that claimed a hosted LangGraph/LangSmith backend and a "falls back to a recorded run" fallback — neither exists in this build (the panel only POSTs to /api/agent, no recorded data is read). Notes now describe the sandbox and point at the README for the go-live story. - MEDIUM: soften "Every tutorial in this repo" in the showcase README — langchain-chat-ui-nvm is the browser buyer front-end, shown inside the LangChain pages rather than as its own entry. - LOW: relabel the paymentPill docblock in types.ts — it is set in content but read by nothing (the panel's pill comes from the intro response); marked legacy. Verified: node lib/demo-agent.mjs self-check passes; tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UxQXzKhM37FPvPuHUnKHh
r-marques
left a comment
There was a problem hiding this comment.
🤖 Automated PR review (round 3) — 🟡 Mergeable with nits
All four round-2 findings are fixed, and I checked each against the code rather than the fold report. The round also brought new work — per-slug sandbox state and WebVTT captions — which I reviewed as new surface rather than waving through. One small residual nit.
Round-2 findings — status
| # | Round-2 finding | Status |
|---|---|---|
| 1 | 🟡 No pull_request trigger; first next build runs after merge |
Fixed |
| 2 | 🟡 latest contradicts the immutableTags=true premise |
Fixed |
| 3 | 🟡 "Make it live" instruction sits above a client-controlled balance | Fixed |
| 4 | 💡 .dockerignore misses Python artifacts |
Fixed |
1 — verified. A credential-free verify job now runs on pull_request, doing npm ci → node lib/demo-agent.mjs → npm run build, and build-push is correctly gated behind both needs: verify and if: github.event_name != 'pull_request'. So a PR now fails on a type error instead of main doing it, and the self-check is finally invoked by something.
2 — verified, and the fix is robust in a way the comment isn't. type=raw,value=latest is gone and the header explains why. Note the resolution is correct under either branch of the ambiguity I raised: if the AR repo really is immutableTags=true, dropping latest avoids the rejected push; if it isn't, dropping it costs nothing, since both the step summary and README already tell you to pin sha-*. The header still asserts immutableTags=true without a citation — that assertion is now load-bearing only for argocd#604, not for this workflow.
3 — verified. The warning now sits with the instruction and names the concrete attack (Cookie: nvm_demo={"x":{"authorized":true,"balance":1e9}}, "httpOnly is not integrity"), which is exactly the gap: an implementer wiring a real backend reads this line.
4 — verified. **/.venv, **/__pycache__, **/*.pyc added with a reason, and the duplicate .git entry removed too.
New surface this round, also checked
- Per-slug cookie state. The legacy-cookie migration discriminates on
typeof parsed.balance !== "number", which is the right test rather than key-presence: it survives anullcookie (short-circuited), a string/number cookie (fails theobjectcheck), and — the interesting case — a tutorial whose slug were literallybalance, since the new shape stores an object there.all[slug]undefined falls through tofreshState(). Sound. - WebVTT captions — a real bug you found yourselves. HTML5
<track>does not accept SRT, so the previous singlesubtitles?: stringnever rendered. All four new.vttpaths resolve, and I regenerated each one from its.srtwith the script's own command: all four are byte-identical to the committed files, so the generated captions are genuinely in sync rather than hand-edited. The two "missing".mp4paths are the gitignored ones the Dockerfile copies fromcatalog/— expected, and verified in round 2.
8d8c0762 at review time, so I am not reporting them green. The new verify job means that now actually means something on this PR.
Verdict: 🟡 Mergeable with nits — 0 blockers, 0 should-fix, 1 nit.
🧭 Inline comments (1)
Each is posted on its line in Files changed.
- 💡 NIT — The SRT→VTT sed rewrites any
<digits>,<3 digits>in caption text, not just timestamps —showcase/scripts/sync-media.sh:20
r-marques
left a comment
There was a problem hiding this comment.
🟡 Approved with comments — no blockers; 0 should-fix, 1 nit(s) left inline.
Inline review: #65 (review)
Addresses r-marques's nit on #65: the unanchored substitution could rewrite a "1,234" in caption text to "1.234". Restrict it to lines containing "-->" (cue timings), so dialogue is never touched. Output is byte-identical for every .vtt in the repo today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UxQXzKhM37FPvPuHUnKHh
The 'verify' job spent ~420s of a ~470-510s run inside `npm ci` — while setup-node's ~/.npm download cache was confirmed HIT (147MB restored). Same 34 packages installed in 22s on a warm run vs 421s on a slow one, identical inputs. The variance is `npm ci` hitting the network for data it already has: registry revalidation and the audit/fund metadata round-trips (the slow runs' output is even missing the "audited 35 packages" line — the audit call hung). Fix targets the actual bottleneck, not a new cache (setup-node's already hits): - `npm ci --prefer-offline --no-audit --no-fund` — install stays offline for cached tarballs; no audit/fund round-trips to stall on. Takes effect on the first run, no second-run warm-up needed. - `concurrency` group per ref with cancel-in-progress scoped to pull_request — superseded PR pushes stop burning a full runner; push/dispatch runs (which publish the immutable image) are never cancelled. Left untouched deliberately: the `npm run build` type-check, the demo-agent self-check, and the entire build-push job (WIF auth, immutable sha-<short> tags, no `latest`, trigger semantics). verify stays credential-free. Not added: a separate actions/cache for showcase/.next/cache — build is only ~32s and isn't the bottleneck; revisit if build growth makes it one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XXfTrdPQAHKYMRUuvhKeYr
Why this matters
Our tutorials repo is the public shop window for building paid AI agents on Nevermined, but today it is a folder of READMEs — a developer has to clone and read each one to grasp what is possible. This PR adds a polished website that presents every tutorial in the same clear shape and lets visitors actually run the payment flow in the browser, so the value lands in seconds instead of a git clone. It is built to become the canonical replacement for examples.nevermined.app and ships with a Docker image ready to deploy on our infrastructure.
What this is
A Next.js (App Router) website under
showcase/. Every current tutorial is presented in one normalized shape — what you'll learn · how it works · under the hood · see it run — behind a persistent left sidebar grouped by protocol (Catalog · x402 HTTP · MCP · LangChain).content/tutorials.ts) generates every page; adding a tutorial is a data edit.402 → authorize → settle) with a per-session credit balance, served by a local sandbox agent (lib/demo-agent.mjs, unit-tested). No external service, no real money, no credentials — it works the moment you open it, with a clean seam to point at a real hosted agent later.catalog/demos (real crypto, multi-chain) embed video + the on-chain receipt; Song From the Headlines also surfaces Rod's alternative take.Deploy
showcase/Dockerfile(Next.js standalone, multi-stage, non-root, listens on$PORTon0.0.0.0) + a repo-root.dockerignore. Build from the repo root so the committedcatalog/*.mp4demo media is in context:Test plan
npm run build— clean, 15 pages (gallery + 10 tutorials + API + 404).node showcase/lib/demo-agent.mjs— sandbox handshake self-check passes./api/agentall serve200; image ~254MB.🤖 Generated with Claude Code
https://claude.ai/code/session_011UxQXzKhM37FPvPuHUnKHh