initial project setup

Fastify + Prisma backend, React + Vite frontend, Docker deployment.
Multi-board feedback platform with anonymous cookie auth, passkey
upgrade path, ALTCHA spam protection, plugin system, and full
privacy-first architecture.
This commit is contained in:
2026-03-19 18:05:16 +02:00
commit f07eddf29e
77 changed files with 7031 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
import { useState, useEffect } from 'react'
import { api } from '../lib/api'
interface DataField {
field: string
purpose: string
retention: string
deletable: boolean
}
interface Manifest {
fields: DataField[]
cookieInfo: string
dataLocation: string
thirdParties: string[]
}
export default function PrivacyPage() {
const [manifest, setManifest] = useState<Manifest | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
api.get<Manifest>('/privacy/data-manifest')
.then(setManifest)
.catch(() => {})
.finally(() => setLoading(false))
}, [])
return (
<div className="max-w-2xl mx-auto px-4 py-8">
<h1
className="text-2xl font-bold mb-2"
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
>
Privacy
</h1>
<p className="text-sm mb-8" style={{ color: 'var(--text-secondary)' }}>
Here is exactly what data this Echoboard instance collects and why.
</p>
{loading ? (
<div className="flex items-center justify-center py-12">
<div
className="w-6 h-6 border-2 rounded-full"
style={{ borderColor: 'var(--border)', borderTopColor: 'var(--accent)', animation: 'spin 0.6s linear infinite' }}
/>
</div>
) : manifest ? (
<>
{/* Quick summary */}
<div className="card p-5 mb-6">
<h2
className="text-base font-semibold mb-3"
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
>
The short version
</h2>
<ul className="flex flex-col gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
<li className="flex items-start gap-2">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
No tracking scripts, no analytics, no third-party cookies
</li>
<li className="flex items-start gap-2">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
All data stays on this server - {manifest.dataLocation}
</li>
<li className="flex items-start gap-2">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
No external fonts or resources loaded
</li>
<li className="flex items-start gap-2">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
You can delete everything at any time
</li>
</ul>
</div>
{/* Cookie info */}
<div className="card p-5 mb-6">
<h2
className="text-base font-semibold mb-2"
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
>
Cookies
</h2>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{manifest.cookieInfo}
</p>
</div>
{/* Data fields */}
<div className="card p-5 mb-6">
<h2
className="text-base font-semibold mb-4"
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
>
What we store
</h2>
<div className="flex flex-col gap-3">
{manifest.fields.map((f) => (
<div
key={f.field}
className="p-3 rounded-lg"
style={{ background: 'var(--bg)' }}
>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium" style={{ color: 'var(--text)' }}>
{f.field}
</span>
{f.deletable && (
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(34, 197, 94, 0.15)', color: 'var(--success)' }}>
deletable
</span>
)}
</div>
<p className="text-xs mb-1" style={{ color: 'var(--text-secondary)' }}>
{f.purpose}
</p>
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>
Retained: {f.retention}
</p>
</div>
))}
</div>
</div>
{/* Third parties */}
{manifest.thirdParties.length > 0 && (
<div className="card p-5 mb-6">
<h2
className="text-base font-semibold mb-2"
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
>
Third parties
</h2>
<ul className="flex flex-col gap-1">
{manifest.thirdParties.map((tp) => (
<li key={tp} className="text-sm" style={{ color: 'var(--text-secondary)' }}>
- {tp}
</li>
))}
</ul>
</div>
)}
</>
) : (
<div className="card p-5">
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
This Echoboard instance is self-hosted and privacy-focused. No tracking, no external services, no data sharing. Your identity is cookie-based unless you register a passkey. You can delete all your data at any time from the settings page.
</p>
</div>
)}
</div>
)
}