mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
refactor: replace Biome with oxlint + oxfmt
Migrate the entire monorepo from Biome 2.4.7 to oxlint 1.56.0 (linter) and oxfmt 0.41.0 (formatter) for faster lint/format and broader rule coverage. - Add `.oxlintrc.json` with React, TypeScript, unicorn, import plugins and correctness/suspicious categories - Add `.oxfmtrc.json` with 2-space indent, import sorting, and Tailwind class sorting (all 30+ custom className attributes migrated) - Add `docs/.oxlintrc.json` and `docs/.oxfmtrc.json` with Next.js plugin - Update all 12 workspace package.json scripts: `oxlint`, `oxfmt`, `oxfmt --check` - Add `format:check` turbo task and CI step - Update VS Code settings/extensions to use `oxc.oxc-vscode` - Update CI path triggers from `biome.json` to new config files - Remove all `biome-ignore` comments and fix shadowed variables - Delete `biome.json` and `docs/biome.json` - Reformat entire codebase with oxfmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||
import Link from "next/link";
|
||||
|
||||
import { TmdbLogo } from "@/components/tmdb-logo";
|
||||
import { baseOptions } from "@/lib/layout.shared";
|
||||
|
||||
@@ -7,8 +8,8 @@ export default function Layout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<HomeLayout {...baseOptions()}>
|
||||
{children}
|
||||
<footer className="border-t border-fd-border px-6 py-8">
|
||||
<div className="mx-auto flex max-w-5xl flex-col items-center gap-4 text-center text-xs text-fd-muted-foreground">
|
||||
<footer className="border-fd-border border-t px-6 py-8">
|
||||
<div className="text-fd-muted-foreground mx-auto flex max-w-5xl flex-col items-center gap-4 text-center text-xs">
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
|
||||
<a
|
||||
href="https://github.com/jakejarvis/sofa"
|
||||
@@ -36,9 +37,8 @@ export default function Layout({ children }: LayoutProps<"/">) {
|
||||
>
|
||||
<TmdbLogo className="h-3" />
|
||||
</a>
|
||||
<p className="max-w-md text-fd-muted-foreground/80">
|
||||
This product uses the TMDB API but is not endorsed or certified by
|
||||
TMDB.
|
||||
<p className="text-fd-muted-foreground/80 max-w-md">
|
||||
This product uses the TMDB API but is not endorsed or certified by TMDB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Smartphone,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ScrollIndicator } from "@/components/scroll-indicator";
|
||||
import { SofaLogo } from "@/components/sofa-logo";
|
||||
|
||||
@@ -21,8 +22,7 @@ const features = [
|
||||
},
|
||||
{
|
||||
title: "Media Server Sync",
|
||||
description:
|
||||
"Automatically log watches from Plex, Jellyfin, or Emby via webhooks.",
|
||||
description: "Automatically log watches from Plex, Jellyfin, or Emby via webhooks.",
|
||||
icon: MonitorPlay,
|
||||
},
|
||||
{
|
||||
@@ -44,8 +44,7 @@ const features = [
|
||||
},
|
||||
{
|
||||
title: "One Container",
|
||||
description:
|
||||
"Deploy with a single Docker image. No external database, no sidecar services.",
|
||||
description: "Deploy with a single Docker image. No external database, no sidecar services.",
|
||||
icon: Container,
|
||||
},
|
||||
];
|
||||
@@ -55,16 +54,16 @@ const steps = [
|
||||
step: "1",
|
||||
title: "Get a TMDB API token",
|
||||
content: (
|
||||
<p className="text-sm text-fd-muted-foreground">
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
Create a free account at{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/settings/api"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-fd-primary underline underline-offset-4 hover:text-fd-primary/80"
|
||||
className="text-fd-primary hover:text-fd-primary/80 underline underline-offset-4"
|
||||
>
|
||||
themoviedb.org
|
||||
<ExternalLinkIcon className="size-3 inline-block translate-y-[-2px] text-fd-muted-foreground ml-1" />
|
||||
<ExternalLinkIcon className="text-fd-muted-foreground ml-1 inline-block size-3 translate-y-[-2px]" />
|
||||
</a>
|
||||
</p>
|
||||
),
|
||||
@@ -73,7 +72,7 @@ const steps = [
|
||||
step: "2",
|
||||
title: "Set three environment variables",
|
||||
content: (
|
||||
<pre className="overflow-x-auto mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
|
||||
<pre className="bg-fd-background/80 text-fd-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 text-xs ring-1 ring-white/[0.06]">
|
||||
<code>{`TMDB_API_READ_ACCESS_TOKEN=...
|
||||
BETTER_AUTH_SECRET=...
|
||||
BETTER_AUTH_URL=https://sofa.example.com`}</code>
|
||||
@@ -84,7 +83,7 @@ BETTER_AUTH_URL=https://sofa.example.com`}</code>
|
||||
step: "3",
|
||||
title: "Start the container",
|
||||
content: (
|
||||
<pre className="overflow-x-auto mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
|
||||
<pre className="bg-fd-background/80 text-fd-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 text-xs ring-1 ring-white/[0.06]">
|
||||
<code>docker compose up -d</code>
|
||||
</pre>
|
||||
),
|
||||
@@ -99,10 +98,10 @@ export default function HomePage() {
|
||||
<div className="pointer-events-none absolute top-[60vh] -right-24 h-[400px] w-[400px] rounded-full bg-[oklch(0.8_0.14_65)] opacity-[0.04] blur-[100px]" />
|
||||
|
||||
{/* ── Hero ── */}
|
||||
<section className="relative flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center px-6 pb-16 pt-8 text-center">
|
||||
<section className="relative flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center px-6 pt-8 pb-16 text-center">
|
||||
<div className="relative flex max-w-2xl flex-col items-center gap-8">
|
||||
<p
|
||||
className="animate-stagger-item text-xs font-medium tracking-[0.3em] text-fd-primary uppercase"
|
||||
className="animate-stagger-item text-fd-primary text-xs font-medium tracking-[0.3em] uppercase"
|
||||
style={{ "--stagger-index": 0 } as React.CSSProperties}
|
||||
>
|
||||
Self-hosted movie & TV tracker
|
||||
@@ -112,7 +111,7 @@ export default function HomePage() {
|
||||
className="animate-stagger-item"
|
||||
style={{ "--stagger-index": 1 } as React.CSSProperties}
|
||||
>
|
||||
<SofaLogo className="size-20 text-fd-primary md:size-28" />
|
||||
<SofaLogo className="text-fd-primary size-20 md:size-28" />
|
||||
</div>
|
||||
|
||||
<h1
|
||||
@@ -123,11 +122,10 @@ export default function HomePage() {
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className="animate-stagger-item max-w-md text-base leading-relaxed text-fd-muted-foreground md:text-lg"
|
||||
className="animate-stagger-item text-fd-muted-foreground max-w-md text-base leading-relaxed md:text-lg"
|
||||
style={{ "--stagger-index": 3 } as React.CSSProperties}
|
||||
>
|
||||
Sofa lives on your own server and seamlessly plugs into your
|
||||
existing home media stack.
|
||||
Sofa lives on your own server and seamlessly plugs into your existing home media stack.
|
||||
</p>
|
||||
|
||||
<div
|
||||
@@ -136,7 +134,7 @@ export default function HomePage() {
|
||||
>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="inline-flex h-11 items-center rounded-lg bg-fd-primary px-7 text-sm font-medium text-fd-primary-foreground transition-shadow hover:shadow-lg hover:shadow-fd-primary/20"
|
||||
className="bg-fd-primary text-fd-primary-foreground hover:shadow-fd-primary/20 inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium transition-shadow hover:shadow-lg"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
@@ -144,7 +142,7 @@ export default function HomePage() {
|
||||
href="https://github.com/jakejarvis/sofa"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium ring-1 ring-fd-border transition-colors hover:bg-fd-accent hover:ring-fd-primary/30"
|
||||
className="ring-fd-border hover:bg-fd-accent hover:ring-fd-primary/30 inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium ring-1 transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
@@ -160,12 +158,12 @@ export default function HomePage() {
|
||||
{features.map((feature, i) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="animate-stagger-item rounded-xl bg-fd-card/50 p-6 ring-1 ring-white/[0.06]"
|
||||
className="animate-stagger-item bg-fd-card/50 rounded-xl p-6 ring-1 ring-white/[0.06]"
|
||||
style={{ "--stagger-index": i } as React.CSSProperties}
|
||||
>
|
||||
<feature.icon className="mb-3 size-5 text-fd-primary" />
|
||||
<h3 className="mb-1.5 font-display text-lg">{feature.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-fd-muted-foreground">
|
||||
<feature.icon className="text-fd-primary mb-3 size-5" />
|
||||
<h3 className="font-display mb-1.5 text-lg">{feature.title}</h3>
|
||||
<p className="text-fd-muted-foreground text-sm leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -176,7 +174,7 @@ export default function HomePage() {
|
||||
{/* ── Quick Start ── */}
|
||||
<section className="mx-auto w-full max-w-3xl px-6 pb-24">
|
||||
<h2
|
||||
className="animate-stagger-item mb-8 text-center font-display text-2xl md:text-3xl"
|
||||
className="animate-stagger-item font-display mb-8 text-center text-2xl md:text-3xl"
|
||||
style={{ "--stagger-index": 0 } as React.CSSProperties}
|
||||
>
|
||||
Deploy in minutes
|
||||
@@ -186,13 +184,11 @@ export default function HomePage() {
|
||||
{steps.map((s) => (
|
||||
<div
|
||||
key={s.step}
|
||||
className="animate-stagger-item rounded-xl bg-fd-card/50 p-5 ring-1 ring-white/[0.06]"
|
||||
style={
|
||||
{ "--stagger-index": Number(s.step) } as React.CSSProperties
|
||||
}
|
||||
className="animate-stagger-item bg-fd-card/50 rounded-xl p-5 ring-1 ring-white/[0.06]"
|
||||
style={{ "--stagger-index": Number(s.step) } as React.CSSProperties}
|
||||
>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-fd-primary text-xs font-semibold text-fd-primary-foreground">
|
||||
<span className="bg-fd-primary text-fd-primary-foreground flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
|
||||
{s.step}
|
||||
</span>
|
||||
<h3 className="font-display text-base">{s.title}</h3>
|
||||
@@ -202,15 +198,15 @@ export default function HomePage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-xs text-fd-muted-foreground flex items-center gap-1 mx-auto w-fit">
|
||||
<InfoIcon className="size-3 inline-block" />
|
||||
<p className="text-fd-muted-foreground mx-auto mt-4 flex w-fit items-center gap-1 text-xs">
|
||||
<InfoIcon className="inline-block size-3" />
|
||||
The first registered account becomes admin.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="text-sm text-fd-primary underline underline-offset-4 hover:text-fd-primary/80"
|
||||
className="text-fd-primary hover:text-fd-primary/80 text-sm underline underline-offset-4"
|
||||
>
|
||||
Read the full setup guide →
|
||||
</Link>
|
||||
|
||||
@@ -8,95 +8,75 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<article className="prose prose-sm prose-invert mx-auto max-w-3xl px-6 py-16 [&_a]:text-fd-primary [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h2]:text-2xl [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h3]:font-display [&_h3]:text-xl [&_h3]:mt-8 [&_h3]:mb-3 [&_p]:text-fd-muted-foreground [&_p]:text-[15px] [&_p]:leading-relaxed [&_li]:text-fd-muted-foreground [&_li]:text-[15px] [&_li]:leading-relaxed [&_ul]:space-y-1 [&_strong]:text-fd-foreground [&_strong]:font-medium">
|
||||
<h1 className="font-display text-3xl tracking-tight mb-2">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p className="text-sm text-fd-muted-foreground/60 !mt-0 mb-8">
|
||||
Last updated: March 16, 2026
|
||||
</p>
|
||||
<article className="prose prose-sm prose-invert [&_a]:text-fd-primary hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h3]:font-display [&_p]:text-fd-muted-foreground [&_li]:text-fd-muted-foreground [&_strong]:text-fd-foreground mx-auto max-w-3xl px-6 py-16 [&_a]:underline [&_a]:underline-offset-4 [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h2]:text-2xl [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-xl [&_li]:text-[15px] [&_li]:leading-relaxed [&_p]:text-[15px] [&_p]:leading-relaxed [&_strong]:font-medium [&_ul]:space-y-1">
|
||||
<h1 className="font-display mb-2 text-3xl tracking-tight">Privacy Policy</h1>
|
||||
<p className="text-fd-muted-foreground/60 !mt-0 mb-8 text-sm">Last updated: March 16, 2026</p>
|
||||
|
||||
<p>
|
||||
Sofa is a self-hosted application. When you run Sofa, your data lives on
|
||||
your own server and is under your control. This policy describes what
|
||||
data Sofa stores, what it sends externally, and how you can control it.
|
||||
Sofa is a self-hosted application. When you run Sofa, your data lives on your own server and
|
||||
is under your control. This policy describes what data Sofa stores, what it sends
|
||||
externally, and how you can control it.
|
||||
</p>
|
||||
|
||||
<h2>Data Stored on Your Server</h2>
|
||||
|
||||
<p>
|
||||
All of the following data is stored locally in a single SQLite database
|
||||
file on the machine running Sofa.
|
||||
All of the following data is stored locally in a single SQLite database file on the machine
|
||||
running Sofa.
|
||||
</p>
|
||||
|
||||
<h3>Account Information</h3>
|
||||
<ul>
|
||||
<li>Email address, display name, and hashed password</li>
|
||||
<li>Session tokens, IP address, and user agent string of active sessions</li>
|
||||
<li>
|
||||
Session tokens, IP address, and user agent string of active sessions
|
||||
</li>
|
||||
<li>
|
||||
If using OIDC/SSO: provider account IDs and OAuth tokens from your
|
||||
identity provider
|
||||
If using OIDC/SSO: provider account IDs and OAuth tokens from your identity provider
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Viewing Activity</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Watchlist status (e.g. “watching”,
|
||||
“completed”) for movies and TV shows
|
||||
Watchlist status (e.g. “watching”, “completed”) for movies and TV
|
||||
shows
|
||||
</li>
|
||||
<li>Individual movie and episode watch timestamps</li>
|
||||
<li>Personal ratings</li>
|
||||
<li>
|
||||
Source of each watch event (manual entry, or imported from Plex,
|
||||
Jellyfin, or Emby)
|
||||
</li>
|
||||
<li>Source of each watch event (manual entry, or imported from Plex, Jellyfin, or Emby)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Media Metadata</h3>
|
||||
<p>
|
||||
Movie, TV show, season, episode, and cast/crew metadata is fetched from{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<a href="https://www.themoviedb.org/" target="_blank" rel="noopener noreferrer">
|
||||
The Movie Database (TMDB)
|
||||
</a>{" "}
|
||||
and cached locally. This includes titles, descriptions, images, genres,
|
||||
streaming availability, and related recommendations. No personal data is
|
||||
included in this metadata.
|
||||
and cached locally. This includes titles, descriptions, images, genres, streaming
|
||||
availability, and related recommendations. No personal data is included in this metadata.
|
||||
</p>
|
||||
|
||||
<h3>Backups</h3>
|
||||
<p>
|
||||
Manual and scheduled backups are full copies of the database stored on
|
||||
your server. They contain all of the data described above. Backup
|
||||
management (creation, download, deletion) requires admin authentication.
|
||||
Manual and scheduled backups are full copies of the database stored on your server. They
|
||||
contain all of the data described above. Backup management (creation, download, deletion)
|
||||
requires admin authentication.
|
||||
</p>
|
||||
|
||||
<h2>External Services</h2>
|
||||
|
||||
<p>
|
||||
Sofa communicates with the following external services during normal
|
||||
operation.
|
||||
</p>
|
||||
<p>Sofa communicates with the following external services during normal operation.</p>
|
||||
|
||||
<h3>The Movie Database (TMDB)</h3>
|
||||
<p>
|
||||
Your server makes API requests to TMDB to fetch and refresh movie and TV
|
||||
metadata. These requests include your TMDB API key and the IDs or search
|
||||
queries for titles being looked up. A single API key is used for the
|
||||
entire instance — TMDB cannot identify individual users. No
|
||||
personal data (watch history, ratings, etc.) is ever sent to TMDB.
|
||||
Your server makes API requests to TMDB to fetch and refresh movie and TV metadata. These
|
||||
requests include your TMDB API key and the IDs or search queries for titles being looked up.
|
||||
A single API key is used for the entire instance — TMDB cannot identify individual
|
||||
users. No personal data (watch history, ratings, etc.) is ever sent to TMDB.
|
||||
</p>
|
||||
<p>
|
||||
When the local image cache is enabled (the default), poster and backdrop
|
||||
images are downloaded from the TMDB CDN and served from your server.
|
||||
When disabled, images are loaded directly from TMDB’s CDN by the
|
||||
client.
|
||||
When the local image cache is enabled (the default), poster and backdrop images are
|
||||
downloaded from the TMDB CDN and served from your server. When disabled, images are loaded
|
||||
directly from TMDB’s CDN by the client.
|
||||
</p>
|
||||
|
||||
<h3>Sofa Public API</h3>
|
||||
@@ -104,78 +84,67 @@ export default function PrivacyPolicyPage() {
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Update checks</strong> — a periodic request to{" "}
|
||||
<code>public-api.sofa.watch</code> to check for new releases. Only a
|
||||
user agent string is sent; no instance or user data is included. Can
|
||||
be disabled in admin settings.
|
||||
<code>public-api.sofa.watch</code> to check for new releases. Only a user agent string is
|
||||
sent; no instance or user data is included. Can be disabled in admin settings.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Telemetry</strong> — an optional, anonymous report sent
|
||||
at most once every 24 hours. It includes a random instance ID, the
|
||||
Sofa version, CPU architecture, bucketed user and title counts (e.g.
|
||||
“2-5”), and which optional features are enabled. No
|
||||
personal data, watch history, or exact counts are included. Telemetry
|
||||
is <strong>disabled by default</strong> and must be explicitly enabled
|
||||
by an admin. See the{" "}
|
||||
<Link href="/docs/telemetry">telemetry documentation</Link> for full
|
||||
details.
|
||||
<strong>Telemetry</strong> — an optional, anonymous report sent at most once every
|
||||
24 hours. It includes a random instance ID, the Sofa version, CPU architecture, bucketed
|
||||
user and title counts (e.g. “2-5”), and which optional features are enabled.
|
||||
No personal data, watch history, or exact counts are included. Telemetry is{" "}
|
||||
<strong>disabled by default</strong> and must be explicitly enabled by an admin. See the{" "}
|
||||
<Link href="/docs/telemetry">telemetry documentation</Link> for full details.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>PostHog (Mobile App Only)</h3>
|
||||
<p>
|
||||
The native iOS and Android app includes optional, anonymous analytics
|
||||
powered by PostHog. This tracks screen views and app lifecycle events
|
||||
— no personal data, search queries, or watch history is collected.
|
||||
Analytics is <strong>disabled by default</strong> and requires explicit
|
||||
opt-in. On iOS, Apple’s App Tracking Transparency prompt is shown
|
||||
first. You can change your preference at any time in the app’s
|
||||
settings.
|
||||
The native iOS and Android app includes optional, anonymous analytics powered by PostHog.
|
||||
This tracks screen views and app lifecycle events — no personal data, search queries,
|
||||
or watch history is collected. Analytics is <strong>disabled by default</strong> and
|
||||
requires explicit opt-in. On iOS, Apple’s App Tracking Transparency prompt is shown
|
||||
first. You can change your preference at any time in the app’s settings.
|
||||
</p>
|
||||
|
||||
<h3>Media Server Integrations</h3>
|
||||
<p>
|
||||
If you connect Plex, Jellyfin, or Emby, those services send webhook
|
||||
events <em>to</em> your Sofa server when you finish watching something.
|
||||
This data is processed and stored locally. Sofa does not send data back
|
||||
to your media servers.
|
||||
If you connect Plex, Jellyfin, or Emby, those services send webhook events <em>to</em> your
|
||||
Sofa server when you finish watching something. This data is processed and stored locally.
|
||||
Sofa does not send data back to your media servers.
|
||||
</p>
|
||||
|
||||
<h3>Sonarr & Radarr</h3>
|
||||
<p>
|
||||
If you use Sonarr or Radarr integration, those services pull your
|
||||
watchlist from Sofa via authenticated API requests. Sofa does not push
|
||||
data to them.
|
||||
If you use Sonarr or Radarr integration, those services pull your watchlist from Sofa via
|
||||
authenticated API requests. Sofa does not push data to them.
|
||||
</p>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<p>
|
||||
Sofa uses a session cookie to keep you logged in. This cookie is
|
||||
HTTP-only, same-site, and contains only a session token. No third-party
|
||||
cookies are set by the web app.
|
||||
Sofa uses a session cookie to keep you logged in. This cookie is HTTP-only, same-site, and
|
||||
contains only a session token. No third-party cookies are set by the web app.
|
||||
</p>
|
||||
|
||||
<h2>Data Sharing</h2>
|
||||
<p>
|
||||
Because Sofa is self-hosted, there is no central service that has access
|
||||
to your data. The project maintainers have no access to your database,
|
||||
your watch history, or your account information. The only data that
|
||||
leaves your server is described in the{" "}
|
||||
Because Sofa is self-hosted, there is no central service that has access to your data. The
|
||||
project maintainers have no access to your database, your watch history, or your account
|
||||
information. The only data that leaves your server is described in the{" "}
|
||||
<a href="#external-services">External Services</a> section above.
|
||||
</p>
|
||||
|
||||
<h2>Data Retention & Deletion</h2>
|
||||
<p>
|
||||
All data is stored in a single SQLite file on your server. You have full
|
||||
control over it:
|
||||
All data is stored in a single SQLite file on your server. You have full control over it:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Admin users can delete other user accounts, which cascades to all
|
||||
associated viewing history and ratings
|
||||
Admin users can delete other user accounts, which cascades to all associated viewing
|
||||
history and ratings
|
||||
</li>
|
||||
<li>
|
||||
Scheduled backups are automatically pruned after a configurable
|
||||
retention limit (default: 7)
|
||||
Scheduled backups are automatically pruned after a configurable retention limit (default:
|
||||
7)
|
||||
</li>
|
||||
<li>Sessions and verification tokens expire automatically</li>
|
||||
<li>Deleting the database file removes all data entirely</li>
|
||||
@@ -183,20 +152,15 @@ export default function PrivacyPolicyPage() {
|
||||
|
||||
<h2>Children</h2>
|
||||
<p>
|
||||
Sofa is not directed at children under 13. Since the application is
|
||||
self-hosted, account creation is controlled entirely by the server
|
||||
administrator.
|
||||
Sofa is not directed at children under 13. Since the application is self-hosted, account
|
||||
creation is controlled entirely by the server administrator.
|
||||
</p>
|
||||
|
||||
<h2>Open Source</h2>
|
||||
<p>
|
||||
Sofa is open source under the MIT License. You can audit exactly what
|
||||
data is collected and how it is handled by reviewing the{" "}
|
||||
<a
|
||||
href="https://github.com/jakejarvis/sofa"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Sofa is open source under the MIT License. You can audit exactly what data is collected and
|
||||
how it is handled by reviewing the{" "}
|
||||
<a href="https://github.com/jakejarvis/sofa" target="_blank" rel="noopener noreferrer">
|
||||
source code
|
||||
</a>
|
||||
.
|
||||
@@ -204,9 +168,8 @@ export default function PrivacyPolicyPage() {
|
||||
|
||||
<h2>Changes</h2>
|
||||
<p>
|
||||
This policy may be updated as new features are added. Changes will be
|
||||
reflected in the “Last updated” date above and committed to
|
||||
the repository.
|
||||
This policy may be updated as new features are added. Changes will be reflected in the
|
||||
“Last updated” date above and committed to the repository.
|
||||
</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFromSource } from "fumadocs-core/search/server";
|
||||
|
||||
import { source } from "@/lib/source";
|
||||
|
||||
export const { GET } = createFromSource(source, {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { createRelativeLink } from "fumadocs-ui/mdx";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getMDXComponents } from "@/components/mdx";
|
||||
import { gitConfig } from "@/lib/layout.shared";
|
||||
import { getPageImage, source } from "@/lib/source";
|
||||
@@ -23,10 +24,8 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription className="mb-0">
|
||||
{page.data.description}
|
||||
</DocsDescription>
|
||||
<div className="flex flex-row gap-2 items-center border-b pb-6">
|
||||
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
|
||||
<div className="flex flex-row items-center gap-2 border-b pb-6">
|
||||
<MarkdownCopyButton markdownUrl={`${page.url}.mdx`} />
|
||||
<ViewOptionsPopover
|
||||
markdownUrl={`${page.url}.mdx`}
|
||||
@@ -49,9 +48,7 @@ export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: PageProps<"/docs/[[...slug]]">,
|
||||
): Promise<Metadata> {
|
||||
export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
||||
|
||||
import { baseOptions } from "@/lib/layout.shared";
|
||||
import { source } from "@/lib/source";
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getLLMText, source } from "@/lib/source";
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: RouteContext<"/llms.mdx/docs/[[...slug]]">,
|
||||
) {
|
||||
export async function GET(_req: Request, { params }: RouteContext<"/llms.mdx/docs/[[...slug]]">) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug);
|
||||
if (!page) notFound();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { llms } from "fumadocs-core/source";
|
||||
|
||||
import { source } from "@/lib/source";
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ImageResponse } from "@takumi-rs/image-response";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getPageImage, source } from "@/lib/source";
|
||||
|
||||
export const revalidate = false;
|
||||
@@ -37,10 +38,7 @@ const PRIMARY = "#fba952";
|
||||
// oklch(0.63 0.02 80) → muted foreground
|
||||
const MUTED = "#90887c";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: RouteContext<"/og/docs/[...slug]">,
|
||||
) {
|
||||
export async function GET(_req: Request, { params }: RouteContext<"/og/docs/[...slug]">) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
@@ -110,9 +108,7 @@ export async function GET(
|
||||
{title}
|
||||
</div>
|
||||
{description ? (
|
||||
<div style={{ fontSize: "24px", lineHeight: 1.5, color: MUTED }}>
|
||||
{description}
|
||||
</div>
|
||||
<div style={{ fontSize: "24px", lineHeight: 1.5, color: MUTED }}>{description}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
|
||||
import { openapi } from "@/lib/openapi";
|
||||
|
||||
import client from "./api-page.client";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, { client });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
|
||||
import { APIPage } from "@/components/api-page";
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents) {
|
||||
|
||||
@@ -43,13 +43,10 @@ export function ScrollIndicator() {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="absolute bottom-9 flex flex-col items-center gap-1.5 text-fd-muted-foreground transition-opacity duration-500"
|
||||
className="text-fd-muted-foreground absolute bottom-9 flex flex-col items-center gap-1.5 transition-opacity duration-500"
|
||||
style={{ opacity: shown && !scrolled && fits ? 1 : 0 }}
|
||||
>
|
||||
<ArrowDown
|
||||
className="size-7 animate-scroll-bounce stroke-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ArrowDown className="animate-scroll-bounce size-7 stroke-2" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
|
||||
|
||||
import { SofaLogo } from "@/components/sofa-logo";
|
||||
|
||||
export const gitConfig = {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { docs } from "collections/server";
|
||||
import { type InferPageType, loader } from "fumadocs-core/source";
|
||||
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
|
||||
import { openapiPlugin } from "fumadocs-openapi/server";
|
||||
|
||||
import { docs } from "collections/server";
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
baseUrl: "/docs",
|
||||
|
||||
+1
-4
@@ -1,10 +1,7 @@
|
||||
import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const { rewrite: rewriteLLM } = rewritePath(
|
||||
"/docs{/*path}",
|
||||
"/llms.mdx/docs{/*path}",
|
||||
);
|
||||
const { rewrite: rewriteLLM } = rewritePath("/docs{/*path}", "/llms.mdx/docs{/*path}");
|
||||
|
||||
export default function proxy(request: NextRequest) {
|
||||
if (isMarkdownPreferred(request)) {
|
||||
|
||||
Reference in New Issue
Block a user