From 200d0841c5b85cf904c28ef71392d689fe2b8eb3 Mon Sep 17 00:00:00 2001
From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Date: Mon, 12 Jan 2026 23:02:36 +0100
Subject: [PATCH 1/5] feat: add legal page and link in footer
- Implement LegalPage component with legal notice and privacy policy
- Add link to legal page in footer
- Update router to include legal route
---
app/src/components/Footer.tsx | 13 ++
app/src/hooks/useUrlSync.ts | 2 +-
app/src/pages/LegalPage.tsx | 292 ++++++++++++++++++++++++++++++++++
app/src/router.tsx | 2 +
4 files changed, 308 insertions(+), 1 deletion(-)
create mode 100644 app/src/pages/LegalPage.tsx
diff --git a/app/src/components/Footer.tsx b/app/src/components/Footer.tsx
index 1125c90a30..05c193b974 100644
--- a/app/src/components/Footer.tsx
+++ b/app/src/components/Footer.tsx
@@ -1,5 +1,6 @@
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
+import { Link as RouterLink } from 'react-router-dom';
import { GITHUB_URL } from '../constants';
interface FooterProps {
@@ -63,6 +64,18 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr
>
stats
+ ·
+
+ legal
+
);
diff --git a/app/src/hooks/useUrlSync.ts b/app/src/hooks/useUrlSync.ts
index 119e8700e5..95fd52f2b5 100644
--- a/app/src/hooks/useUrlSync.ts
+++ b/app/src/hooks/useUrlSync.ts
@@ -6,7 +6,7 @@
import { useEffect } from 'react';
-import type { FilterCategory, ActiveFilters } from '../types';
+import type { ActiveFilters } from '../types';
import { FILTER_CATEGORIES } from '../types';
/**
diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx
new file mode 100644
index 0000000000..437a5ba448
--- /dev/null
+++ b/app/src/pages/LegalPage.tsx
@@ -0,0 +1,292 @@
+import { useEffect } from 'react';
+import { Helmet } from 'react-helmet-async';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Link from '@mui/material/Link';
+import Paper from '@mui/material/Paper';
+import Table from '@mui/material/Table';
+import TableBody from '@mui/material/TableBody';
+import TableCell from '@mui/material/TableCell';
+import TableRow from '@mui/material/TableRow';
+
+import { useAnalytics } from '../hooks';
+import { Breadcrumb, Footer } from '../components';
+import { GITHUB_URL } from '../constants';
+
+export function LegalPage() {
+ const { trackPageview, trackEvent } = useAnalytics();
+
+ useEffect(() => {
+ trackPageview('/legal');
+ }, [trackPageview]);
+
+ const headingStyle = {
+ fontFamily: '"MonoLisa", monospace',
+ fontWeight: 600,
+ fontSize: '1rem',
+ color: '#1f2937',
+ mb: 2,
+ };
+
+ const subheadingStyle = {
+ fontFamily: '"MonoLisa", monospace',
+ fontWeight: 600,
+ fontSize: '1.1rem',
+ color: '#374151',
+ mt: 3,
+ mb: 1,
+ };
+
+ const textStyle = {
+ fontFamily: '"MonoLisa", monospace',
+ fontSize: '0.9rem',
+ color: '#4b5563',
+ lineHeight: 1.8,
+ mb: 2,
+ };
+
+ const tableStyle = {
+ '& .MuiTableCell-root': {
+ fontFamily: '"MonoLisa", monospace',
+ fontSize: '0.85rem',
+ color: '#4b5563',
+ borderBottom: '1px solid #f3f4f6',
+ py: 1.5,
+ px: 2,
+ },
+ '& .MuiTableCell-root:first-of-type': {
+ fontWeight: 500,
+ color: '#374151',
+ width: '40%',
+ },
+ };
+
+ return (
+ <>
+
+ legal | pyplots.ai
+
+
+
+
+
+
+
+
+ {/* Legal Notice */}
+
+
+ Legal Notice
+
+
+
+ Operator
+
+ Markus Neusinger
+
+ Visp, Switzerland
+
+
+
+ Contact
+
+ Email:{' '}
+
+ admin@pyplots.ai
+
+
+
+
+ Disclaimer
+
+ This is a personal portfolio project, not a commercial service. The content is provided "as is"
+ without warranty of any kind. Code examples are for educational purposes and should be reviewed before use
+ in production environments.
+
+
+
+ {/* Privacy Policy */}
+
+
+ Privacy Policy
+
+
+ Data Controller
+ Markus Neusinger (see Legal Notice above)
+
+ What We Collect
+
+ Anonymized Analytics: We use Plausible Analytics, a privacy-focused analytics tool. It
+ collects no personal data, uses no cookies, and does not track you across websites. All data is aggregated
+ and anonymous.
+
+
+ Server Logs: Temporary technical logs (anonymized IP addresses) are retained for up to 30
+ days for security and debugging purposes.
+
+
+ What We Do NOT Collect
+
+ • No user accounts or personal profiles
+
+ • No personal data (names, emails, etc.)
+
+ • No cookies (except technically necessary session cookies)
+ • No AI training: Your interactions are not used to train AI models
+
+
+
+ Analytics: Filter selections and search terms are tracked anonymously via Plausible
+ Analytics for usage statistics. This data is aggregated and cannot be linked to individual users.
+
+
+
+ Contributors: If you suggest a plot type via GitHub, your GitHub username may be credited
+ in the specification metadata. This is public information from your GitHub profile.
+
+
+ Hosting & Third Parties
+ All services are hosted in the EU (Netherlands, europe-west4):
+
+
+
+ Hosting
+ Google Cloud Run (Netherlands)
+
+
+ Database
+ Google Cloud SQL (Netherlands)
+
+
+ Storage
+ Google Cloud Storage (Netherlands)
+
+
+ Analytics
+ Plausible Analytics (EU, proxied)
+
+
+
+
+ Your Rights
+
+ Under GDPR and Swiss DSG, you have the right to access, rectification, erasure, and data portability. Since
+ we do not store personal data, there is typically nothing to delete or export. For questions, contact{' '}
+
+ admin@pyplots.ai
+
+ .
+
+
+
+ {/* Transparency */}
+
+
+ Transparency
+
+
+
+ This project is open source and committed to full transparency about how it works and what it costs.
+
+
+ Technology Stack
+
+
+
+ Typography
+
+
+ MonoLisa
+ {' '}
+ by Marcus Sterz
+
+
+
+ Frontend
+ React 19, Vite, MUI 7, TypeScript
+
+
+ Backend
+ Python 3.13, FastAPI, SQLAlchemy
+
+
+ Database
+ PostgreSQL
+
+
+ Hosting
+ Google Cloud Run (Netherlands)
+
+
+ Storage
+ Google Cloud Storage
+
+
+ Analytics
+ Plausible (privacy-friendly, no cookies)
+
+
+ AI
+ Anthropic Claude via Claude Max (code generation & review)
+
+
+
+
+ Source Code
+
+ The entire codebase is publicly available under the MIT License:
+
+
+ github.com/MarkusNeusinger/pyplots
+
+
+
+ Monthly Costs (approximate)
+
+
+ Claude Max subscription is shared across projects.
+
+ All costs are currently covered privately. Last updated: January 2026.
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/src/router.tsx b/app/src/router.tsx
index 98eec870c0..c6d5aa4760 100644
--- a/app/src/router.tsx
+++ b/app/src/router.tsx
@@ -7,6 +7,7 @@ import { SpecPage } from './pages/SpecPage';
import { CatalogPage } from './pages/CatalogPage';
import { InteractivePage } from './pages/InteractivePage';
import { DebugPage } from './pages/DebugPage';
+import { LegalPage } from './pages/LegalPage';
const router = createBrowserRouter([
{
@@ -15,6 +16,7 @@ const router = createBrowserRouter([
children: [
{ index: true, element: },
{ path: 'catalog', element: },
+ { path: 'legal', element: },
{ path: ':specId', element: },
{ path: ':specId/:library', element: },
],
From c1140920945591634116f1a2afe8d1f779407b50 Mon Sep 17 00:00:00 2001
From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Date: Mon, 12 Jan 2026 23:05:43 +0100
Subject: [PATCH 2/5] feat: add legal page and update sitemap
- Implement bot-optimized legal page with appropriate og:tags
- Add legal page URL to sitemap
- Update documentation to include legal page in routes
---
api/routers/seo.py | 14 ++++++++++++++
docs/reference/plausible.md | 1 +
docs/reference/seo.md | 7 +++++--
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/api/routers/seo.py b/api/routers/seo.py
index e45ac25428..7125b7e122 100644
--- a/api/routers/seo.py
+++ b/api/routers/seo.py
@@ -71,6 +71,7 @@ async def get_sitemap(db: AsyncSession | None = Depends(optional_db)):
'',
" https://pyplots.ai/",
" https://pyplots.ai/catalog",
+ " https://pyplots.ai/legal",
]
# Add spec URLs (overview + all implementations)
@@ -131,6 +132,19 @@ async def seo_catalog():
)
+@router.get("/seo-proxy/legal")
+async def seo_legal():
+ """Bot-optimized legal page with correct og:tags."""
+ return HTMLResponse(
+ BOT_HTML_TEMPLATE.format(
+ title="Legal | pyplots.ai",
+ description="Legal notice, privacy policy, and transparency information for pyplots.ai",
+ image=DEFAULT_HOME_IMAGE,
+ url="https://pyplots.ai/legal",
+ )
+ )
+
+
@router.get("/seo-proxy/{spec_id}")
async def seo_spec_overview(spec_id: str, db: AsyncSession | None = Depends(optional_db)):
"""Bot-optimized spec overview page with collage og:image."""
diff --git a/docs/reference/plausible.md b/docs/reference/plausible.md
index 509c5ff99d..b369f14064 100644
--- a/docs/reference/plausible.md
+++ b/docs/reference/plausible.md
@@ -42,6 +42,7 @@ https://pyplots.ai/{category}/{value}/{category}/{value}/...
|-----|-------------|
| `/` | Home page (no filters) |
| `/catalog` | Catalog page (alphabetical spec list) |
+| `/legal` | Legal notice, privacy policy, transparency |
| `/{spec_id}` | Spec overview page (grid of all implementations) |
| `/{spec_id}/{library}` | Spec detail page (single library implementation) |
| `/interactive/{spec_id}/{library}` | Interactive fullscreen view (HTML plots) |
diff --git a/docs/reference/seo.md b/docs/reference/seo.md
index a9924cde95..b5b2488586 100644
--- a/docs/reference/seo.md
+++ b/docs/reference/seo.md
@@ -137,6 +137,7 @@ Backend endpoints that serve HTML with correct meta tags for bots.
|----------|---------|----------|
| `GET /seo-proxy/` | Home page | Default (`og-image.png`) |
| `GET /seo-proxy/catalog` | Catalog page | Default |
+| `GET /seo-proxy/legal` | Legal page | Default |
| `GET /seo-proxy/{spec_id}` | Spec overview | Collage (2x3 grid) |
| `GET /seo-proxy/{spec_id}/{library}` | Implementation | Single branded |
@@ -253,6 +254,7 @@ Dynamic XML sitemap for search engine indexing.
https://pyplots.ai/https://pyplots.ai/catalog
+ https://pyplots.ai/legalhttps://pyplots.ai/{spec_id}https://pyplots.ai/{spec_id}/{library}
@@ -264,8 +266,9 @@ Dynamic XML sitemap for search engine indexing.
1. Home page (`/`)
2. Catalog page (`/catalog`)
-3. Spec overview pages (`/{spec_id}`) - only if spec has implementations
-4. Implementation pages (`/{spec_id}/{library}`) - all implementations
+3. Legal page (`/legal`)
+4. Spec overview pages (`/{spec_id}`) - only if spec has implementations
+5. Implementation pages (`/{spec_id}/{library}`) - all implementations
### nginx Proxy
From 09b9881005b0b3ec80dd03e63aaf2fed9b6c82b4 Mon Sep 17 00:00:00 2001
From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Date: Tue, 13 Jan 2026 22:17:10 +0100
Subject: [PATCH 3/5] feat: update legal page content and styles
- Adjust font sizes for headings and subheadings
- Modify breadcrumb margin for better spacing
- Add links for Legal Notice, Privacy Policy, and Transparency sections
- Enhance disclaimer and analytics descriptions
- Update monthly costs section with accurate figures
- Improve overall layout and styling for better readability
---
app/src/pages/LegalPage.tsx | 180 +++++++++++++++++++++++++++---------
1 file changed, 134 insertions(+), 46 deletions(-)
diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx
index 437a5ba448..87c24680e6 100644
--- a/app/src/pages/LegalPage.tsx
+++ b/app/src/pages/LegalPage.tsx
@@ -23,7 +23,7 @@ export function LegalPage() {
const headingStyle = {
fontFamily: '"MonoLisa", monospace',
fontWeight: 600,
- fontSize: '1rem',
+ fontSize: '1.25rem',
color: '#1f2937',
mb: 2,
};
@@ -31,7 +31,7 @@ export function LegalPage() {
const subheadingStyle = {
fontFamily: '"MonoLisa", monospace',
fontWeight: 600,
- fontSize: '1.1rem',
+ fontSize: '1rem',
color: '#374151',
mt: 3,
mb: 1,
@@ -57,7 +57,7 @@ export function LegalPage() {
'& .MuiTableCell-root:first-of-type': {
fontWeight: 500,
color: '#374151',
- width: '40%',
+ width: '25%',
},
};
@@ -70,7 +70,19 @@ export function LegalPage() {
-
+
+
+
+
+ Legal Notice
+
+
+ Privacy Policy
+
+
+ Transparency
+
+
{/* Legal Notice */}
@@ -99,9 +111,11 @@ export function LegalPage() {
Disclaimer
- This is a personal portfolio project, not a commercial service. The content is provided "as is"
- without warranty of any kind. Code examples are for educational purposes and should be reviewed before use
- in production environments.
+ This is a personal portfolio project showcasing Python visualization examples, generated and maintained
+ through AI-powered workflows. All code examples are meant for inspiration and learning – take them as a
+ starting point, adapt them to your data and requirements, or use AI tools to customize them for your
+ specific needs. Code is provided "as is" under the MIT License and should be reviewed before
+ production use.
@@ -116,13 +130,29 @@ export function LegalPage() {
What We Collect
- Anonymized Analytics: We use Plausible Analytics, a privacy-focused analytics tool. It
- collects no personal data, uses no cookies, and does not track you across websites. All data is aggregated
- and anonymous.
+ Anonymized Analytics: We use{' '}
+
+ Plausible Analytics
+
+ , a privacy-focused analytics tool. It collects no personal data, uses no cookies, and does not track you
+ across websites. We track: page views, navigation patterns, code copies, image downloads, search queries,
+ filter usage, and UI interactions. When you share a link, we detect which platform requests the preview
+ (e.g., LinkedIn, WhatsApp). All data is aggregated and anonymous.
+
+
+ Public Dashboard: Our analytics are{' '}
+
+ fully public
+ {' '}
+ – see exactly what we see.
- Server Logs: Temporary technical logs (anonymized IP addresses) are retained for up to 30
- days for security and debugging purposes.
+ Server Logs: Technical server logs including IP addresses, request URLs, and user agents
+ are retained for 30 days via{' '}
+
+ Google Cloud Logging
+ {' '}
+ for security and debugging purposes.
What We Do NOT Collect
@@ -131,15 +161,10 @@ export function LegalPage() {
• No personal data (names, emails, etc.)
- • No cookies (except technically necessary session cookies)
+ • No cookies at all (we use localStorage for UI preferences only)
• No AI training: Your interactions are not used to train AI models
-
- Analytics: Filter selections and search terms are tracked anonymously via Plausible
- Analytics for usage statistics. This data is aggregated and cannot be linked to individual users.
-
-
Contributors: If you suggest a plot type via GitHub, your GitHub username may be credited
in the specification metadata. This is public information from your GitHub profile.
@@ -193,41 +218,109 @@ export function LegalPage() {
- Typography
+ Editor
-
- MonoLisa
- {' '}
- by Marcus Sterz
+
+ JetBrains PyCharm
+
Frontend
- React 19, Vite, MUI 7, TypeScript
+
+
+ React
+ {' '}
+ 19,{' '}
+
+ Vite
+
+ ,{' '}
+
+ MUI
+ {' '}
+ 7,{' '}
+
+ TypeScript
+
+ Backend
- Python 3.13, FastAPI, SQLAlchemy
+
+
+ Python
+ {' '}
+ 3.13,{' '}
+
+ FastAPI
+
+ ,{' '}
+
+ SQLAlchemy
+
+ Database
- PostgreSQL
+
+
+ PostgreSQL
+ {' '}
+ 18
+ Hosting
- Google Cloud Run (Netherlands)
+
+
+ Google Cloud Run
+ {' '}
+ (Netherlands)
+ Storage
- Google Cloud Storage
+
+
+ Google Cloud Storage
+
+ Analytics
- Plausible (privacy-friendly, no cookies)
+
+
+ Plausible
+ {' '}
+ (privacy-friendly, no cookies,{' '}
+
+ public dashboard
+
+ )
+
+
+
+ Code Generation
+
+
+ Anthropic Claude
+ {' '}
+ (code generation & review),{' '}
+
+ GitHub Copilot
+ {' '}
+ (PR reviews)
+
- AI
- Anthropic Claude via Claude Max (code generation & review)
+ Typography
+
+
+ MonoLisa
+ {' '}
+ by Marcus Sterz
+
@@ -250,40 +343,35 @@ export function LegalPage() {
Cloud SQL
- ~$40
+ ~$10Cloud Storage
- ~$5
-
-
- Plausible Analytics
- ~$9
+ ~$1Domain (.ai)~$8
-
- Claude Max
- ~$100 (shared)
- Total
- ~$177/month
+ ~$34/month
- Claude Max subscription is shared across projects.
-
- All costs are currently covered privately. Last updated: January 2026.
+ Direct hosting costs only. Subscriptions (GitHub Pro, Plausible, Claude MAX, PyCharm, etc.) are shared
+ across projects. All costs are currently covered privately.
+
+
+ Last updated: January 2026
+
From 9abe18ce0781ee6f66a67492302eabd90f5ae981 Mon Sep 17 00:00:00 2001
From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Date: Tue, 13 Jan 2026 22:26:27 +0100
Subject: [PATCH 4/5] feat: update legal page rights information
- Clarified rights under GDPR and Swiss DSG
- Improved wording for better readability
---
app/src/pages/LegalPage.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx
index 87c24680e6..54a6a631d4 100644
--- a/app/src/pages/LegalPage.tsx
+++ b/app/src/pages/LegalPage.tsx
@@ -195,8 +195,8 @@ export function LegalPage() {
Your Rights
- Under GDPR and Swiss DSG, you have the right to access, rectification, erasure, and data portability. Since
- we do not store personal data, there is typically nothing to delete or export. For questions, contact{' '}
+ You have the right to access, rectify, erase, and export your data. Since we do not store personal data,
+ there is typically nothing to delete or export. For questions, contact{' '}
admin@pyplots.ai
From fd602e2bc6c602323db60a50666f0643adbb6901 Mon Sep 17 00:00:00 2001
From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Date: Tue, 13 Jan 2026 22:32:43 +0100
Subject: [PATCH 5/5] feat: update legal page and footer tracking
- Adjust legal page component in footer to include tracking event
- Update cloud SQL version in copilot instructions
---
.github/copilot-instructions.md | 14 +++++++-------
app/src/components/Footer.tsx | 1 +
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 4bd0247407..713c9cf9d0 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -219,14 +219,14 @@ plt.savefig('plot.png', dpi=300, bbox_inches='tight')
The project runs on **Google Cloud Platform** (europe-west4 region):
-| Service | Component | Purpose |
-|---------|-----------|---------|
-| **Cloud Run** | `pyplots-backend` | FastAPI API (auto-scaling, serverless) |
+| Service | Component | Purpose |
+|---------|--------------------|---------|
+| **Cloud Run** | `pyplots-backend` | FastAPI API (auto-scaling, serverless) |
| **Cloud Run** | `pyplots-frontend` | React SPA served via nginx |
-| **Cloud SQL** | PostgreSQL 15 | Database (Unix socket in production) |
-| **Cloud Storage** | `pyplots-images` | Preview images (GCS bucket) |
-| **Secret Manager** | `DATABASE_URL` | Secure credential storage |
-| **Cloud Build** | Triggers | Auto-deploy on push to main |
+| **Cloud SQL** | PostgreSQL 18 | Database (Unix socket in production) |
+| **Cloud Storage** | `pyplots-images` | Preview images (GCS bucket) |
+| **Secret Manager** | `DATABASE_URL` | Secure credential storage |
+| **Cloud Build** | Triggers | Auto-deploy on push to main |
Automatic deployment on push to `main`:
- `api/**`, `core/**`, `pyproject.toml` changes → Backend redeploy
diff --git a/app/src/components/Footer.tsx b/app/src/components/Footer.tsx
index 05c193b974..eb984a1bba 100644
--- a/app/src/components/Footer.tsx
+++ b/app/src/components/Footer.tsx
@@ -68,6 +68,7 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr
onTrackEvent?.('internal_link', { destination: 'legal', spec: selectedSpec, library: selectedLibrary })}
sx={{
color: '#9ca3af',
textDecoration: 'none',