Skip to content

Commit a26e74e

Browse files
committed
Support static notebook export & charts
Add CI workflows to build/deploy and schedule weekly data refresh; add an export script for notebooks. Switch Marimo iframe to load exported static HTML (HEAD check for 404), improve error UI, and remove local marimo server dependency. Convert insight pages to client components and simplify rendering, adjust layout scrolling. Add reusable chart UI components (DataTable, PlotlyChart, StatCard) and enable Next.js static export + unoptimized images. Update app package scripts and add Plotly dependencies; update .gitignore to ignore exported notebook assets. Various data/notebook model updates and other housekeeping (removed serve_notebooks.py).
1 parent 79ea111 commit a26e74e

33 files changed

Lines changed: 5356 additions & 2706 deletions

.github/workflows/deploy.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Build and deploy
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
workflow_call:
8+
9+
jobs:
10+
build-deploy:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 2
16+
17+
- uses: astral-sh/setup-uv@v4
18+
- run: uv python install 3.13
19+
20+
- name: Install Python deps
21+
run: uv sync
22+
23+
- name: Check if notebooks changed
24+
id: changed
25+
run: |
26+
git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^notebooks/' \
27+
&& echo "notebooks=true" >> $GITHUB_OUTPUT \
28+
|| echo "notebooks=false" >> $GITHUB_OUTPUT
29+
30+
- name: Export notebooks
31+
if: steps.changed.outputs.notebooks == 'true' || github.event_name != 'push'
32+
env:
33+
OSO_API_KEY: ${{ secrets.OSO_API_KEY }}
34+
run: uv run python scripts/export_notebooks.py
35+
36+
- uses: pnpm/action-setup@v4
37+
with:
38+
version: 10
39+
40+
- uses: actions/setup-node@v4
41+
with:
42+
node-version: '20'
43+
cache: 'pnpm'
44+
cache-dependency-path: app/pnpm-lock.yaml
45+
46+
- name: Install Node deps
47+
run: cd app && pnpm install
48+
49+
- name: Build Next.js
50+
run: cd app && pnpm build

.github/workflows/refresh-data.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: Weekly data refresh
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * 0' # Sunday 3am UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
refresh:
10+
uses: ./.github/workflows/deploy.yml
11+
secrets: inherit

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ __pycache__/
1414
# --- Environment variables ---
1515
.env
1616
.env.local
17+
app/.env.*
1718

1819
# --- Next.js (app/) ---
1920
app/.next/
2021
app/out/
2122
app/node_modules/
23+
app/public/notebooks/
24+
app/public/data/
2225

2326
# --- OS ---
2427
.DS_Store
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1+
'use client';
12
import MarimoIframe from '@/components/MarimoIframe';
2-
33
export default function DefiDeveloperJourneys() {
4-
return (
5-
<div className="h-full w-full">
6-
<MarimoIframe notebookName="notebooks/insights/defi-developer-journeys" />
7-
</div>
8-
);
4+
return <MarimoIframe notebookName="notebooks/insights/defi-developer-journeys" />;
95
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1+
'use client';
12
import MarimoIframe from '@/components/MarimoIframe';
2-
33
export default function DeveloperLifecycle() {
4-
return (
5-
<div className="h-full w-full">
6-
<MarimoIframe notebookName="notebooks/insights/developer-lifecycle" />
7-
</div>
8-
);
4+
return <MarimoIframe notebookName="notebooks/insights/developer-lifecycle" />;
95
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1+
'use client';
12
import MarimoIframe from '@/components/MarimoIframe';
2-
33
export default function DeveloperReport2025() {
4-
return (
5-
<div className="h-full w-full">
6-
<MarimoIframe notebookName="notebooks/insights/developer-report-2025" />
7-
</div>
8-
);
4+
return <MarimoIframe notebookName="notebooks/insights/developer-report-2025" />;
95
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1+
'use client';
12
import MarimoIframe from '@/components/MarimoIframe';
2-
33
export default function DeveloperRetention() {
4-
return (
5-
<div className="h-full w-full">
6-
<MarimoIframe notebookName="notebooks/insights/developer-retention" />
7-
</div>
8-
);
4+
return <MarimoIframe notebookName="notebooks/insights/developer-retention" />;
95
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1+
'use client';
12
import MarimoIframe from '@/components/MarimoIframe';
2-
33
export default function SpeedrunEthereum() {
4-
return (
5-
<div className="h-full w-full">
6-
<MarimoIframe notebookName="notebooks/insights/speedrun-ethereum" />
7-
</div>
8-
);
4+
return <MarimoIframe notebookName="notebooks/insights/speedrun-ethereum" />;
95
}

app/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function RootLayout({
1717
<body className="bg-white">
1818
<div className="flex h-screen overflow-hidden">
1919
<Sidebar />
20-
<main className="flex-1 flex flex-col overflow-hidden bg-white">
20+
<main className="flex-1 flex flex-col overflow-y-auto bg-white">
2121
{children}
2222
</main>
2323
</div>

app/components/MarimoIframe.tsx

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,33 @@ import { useEffect, useState } from 'react';
44

55
interface MarimoIframeProps {
66
notebookName: string;
7-
marimoPort?: number;
87
}
98

10-
export default function MarimoIframe({ notebookName, marimoPort = 8000 }: MarimoIframeProps) {
9+
export default function MarimoIframe({ notebookName }: MarimoIframeProps) {
1110
const [isLoading, setIsLoading] = useState(true);
1211
const [hasError, setHasError] = useState(false);
13-
// Remove .py extension if present, and construct URL
12+
// Remove .py extension if present, and construct URL to static exported HTML
1413
const notebookPath = notebookName.replace(/\.py$/, '');
15-
const marimoUrl = `http://localhost:${marimoPort}/${notebookPath}`;
14+
const notebookUrl = `/${notebookPath}.html`;
1615

1716
useEffect(() => {
1817
setIsLoading(true);
1918
setHasError(false);
20-
}, [notebookName]);
19+
// Use fetch HEAD to detect 404s — iframe onError only fires for network failures,
20+
// not HTTP error responses, so a missing file would silently show a 404 page.
21+
fetch(notebookUrl, { method: 'HEAD' })
22+
.then(res => {
23+
if (!res.ok) {
24+
setHasError(true);
25+
setIsLoading(false);
26+
}
27+
// If ok, let the iframe's onLoad handle clearing the loading state
28+
})
29+
.catch(() => {
30+
setHasError(true);
31+
setIsLoading(false);
32+
});
33+
}, [notebookUrl]);
2134

2235
const handleLoad = () => {
2336
setIsLoading(false);
@@ -45,24 +58,23 @@ export default function MarimoIframe({ notebookName, marimoPort = 8000 }: Marimo
4558
</div>
4659
<div className="text-red-600 font-semibold text-lg mb-2">Failed to load notebook</div>
4760
<div className="text-gray-600 text-sm mb-6 text-center max-w-md">
48-
Make sure the marimo server is running on port {marimoPort}
61+
Notebook not exported yet — run <code className="bg-gray-100 px-1 rounded">pnpm export:notebooks</code>
4962
</div>
5063
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-left max-w-md w-full">
51-
<div className="text-xs font-semibold text-gray-500 mb-2">Expected URL:</div>
52-
<code className="text-xs text-gray-700 break-all">{marimoUrl}</code>
53-
</div>
54-
<div className="mt-6 text-gray-500 text-sm">
55-
Run: <code className="bg-gray-100 px-2 py-1 rounded text-xs">uv run python serve_notebooks.py</code> from the <code className="bg-gray-100 px-2 py-1 rounded text-xs">ddp</code> directory
64+
<div className="text-xs font-semibold text-gray-500 mb-2">Expected path:</div>
65+
<code className="text-xs text-gray-700 break-all">{notebookUrl}</code>
5666
</div>
5767
</div>
5868
)}
59-
<iframe
60-
src={marimoUrl}
61-
className="w-full h-full border-0"
62-
onLoad={handleLoad}
63-
onError={handleError}
64-
title={notebookName}
65-
/>
69+
{!hasError && (
70+
<iframe
71+
src={notebookUrl}
72+
className="w-full h-full border-0"
73+
onLoad={handleLoad}
74+
onError={handleError}
75+
title={notebookName}
76+
/>
77+
)}
6678
</div>
6779
);
6880
}

0 commit comments

Comments
 (0)