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/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/app/src/components/Footer.tsx b/app/src/components/Footer.tsx index 1125c90a30..eb984a1bba 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,19 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr > stats + · + onTrackEvent?.('internal_link', { destination: 'legal', spec: selectedSpec, library: selectedLibrary })} + sx={{ + color: '#9ca3af', + textDecoration: 'none', + '&:hover': { color: '#6b7280' }, + }} + > + 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..54a6a631d4 --- /dev/null +++ b/app/src/pages/LegalPage.tsx @@ -0,0 +1,380 @@ +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: '1.25rem', + color: '#1f2937', + mb: 2, + }; + + const subheadingStyle = { + fontFamily: '"MonoLisa", monospace', + fontWeight: 600, + fontSize: '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: '25%', + }, + }; + + return ( + <> + + legal | pyplots.ai + + + + + + + + + + Legal Notice + + + Privacy Policy + + + Transparency + + + + + {/* Legal Notice */} + + + Legal Notice + + + + Operator +
+ Markus Neusinger +
+ Visp, Switzerland +
+ + + Contact +
+ Email:{' '} + + admin@pyplots.ai + +
+ + + Disclaimer +
+ 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. +
+
+ + {/* 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. 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: 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 + + • No user accounts or personal profiles +
+ • No personal data (names, emails, etc.) +
+ • No cookies at all (we use localStorage for UI preferences only) +
No AI training: Your interactions are not used to train AI models +
+ + + 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 + + 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 + + . + +
+ + {/* Transparency */} + + + Transparency + + + + This project is open source and committed to full transparency about how it works and what it costs. + + + Technology Stack + + + + Editor + + + JetBrains PyCharm + + + + + Frontend + + + React + {' '} + 19,{' '} + + Vite + + ,{' '} + + MUI + {' '} + 7,{' '} + + TypeScript + + + + + Backend + + + Python + {' '} + 3.13,{' '} + + FastAPI + + ,{' '} + + SQLAlchemy + + + + + Database + + + PostgreSQL + {' '} + 18 + + + + Hosting + + + Google Cloud Run + {' '} + (Netherlands) + + + + Storage + + + Google Cloud Storage + + + + + Analytics + + + Plausible + {' '} + (privacy-friendly, no cookies,{' '} + + public dashboard + + ) + + + + Code Generation + + + Anthropic Claude + {' '} + (code generation & review),{' '} + + GitHub Copilot + {' '} + (PR reviews) + + + + Typography + + + MonoLisa + {' '} + by Marcus Sterz + + + +
+ + Source Code + + The entire codebase is publicly available under the MIT License: +
+ + github.com/MarkusNeusinger/pyplots + +
+ + Monthly Hosting Costs (approximate) + + + + Cloud Run + ~$15 + + + Cloud SQL + ~$10 + + + Cloud Storage + ~$1 + + + Domain (.ai) + ~$8 + + + + Total + + + ~$34/month + + + +
+ + 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 + +
+ +