Files
sofa/app/api/feed/stats/route.ts
T
jakeandClaude Opus 4.6 96213c3086 Add Docker packaging and migrate from better-sqlite3 to libsql
Docker self-hosting support:
- Dockerfile (multi-stage Alpine build with tini init)
- docker-compose.yml with named volume for SQLite persistence
- /api/health endpoint for container health checks
- Auto-migration on startup via drizzle-orm/libsql/migrator
- Graceful shutdown (SIGTERM stops scheduler, closes DB)
- Next.js standalone output mode for minimal image size

Database driver migration (better-sqlite3 → @libsql/client):
- Eliminates native C++ compilation, enabling Alpine Docker images
- All DB queries converted from sync to async across services and routes
- DATABASE_URL now uses libsql file: prefix format
- drizzle.config.ts dialect changed to turso for libsql support
- Initial migration files generated in drizzle/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:14:57 -05:00

76 lines
2.1 KiB
TypeScript

import { and, eq, sql } from "drizzle-orm";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth/server";
import { db } from "@/lib/db/client";
import {
userEpisodeWatches,
userMovieWatches,
userTitleStatus,
} from "@/lib/db/schema";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = session.user.id;
const now = new Date();
// Start of current month
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
// Start of current week (Monday)
const dayOfWeek = now.getDay();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
weekStart.setHours(0, 0, 0, 0);
const [moviesThisMonth] = await db
.select({ count: sql<number>`count(*)` })
.from(userMovieWatches)
.where(
and(
eq(userMovieWatches.userId, userId),
sql`${userMovieWatches.watchedAt} >= ${Math.floor(monthStart.getTime() / 1000)}`,
),
)
.all();
const [episodesThisWeek] = await db
.select({ count: sql<number>`count(*)` })
.from(userEpisodeWatches)
.where(
and(
eq(userEpisodeWatches.userId, userId),
sql`${userEpisodeWatches.watchedAt} >= ${Math.floor(weekStart.getTime() / 1000)}`,
),
)
.all();
const [librarySize] = await db
.select({ count: sql<number>`count(*)` })
.from(userTitleStatus)
.where(eq(userTitleStatus.userId, userId))
.all();
const [completedCount] = await db
.select({ count: sql<number>`count(*)` })
.from(userTitleStatus)
.where(
and(
eq(userTitleStatus.userId, userId),
eq(userTitleStatus.status, "completed"),
),
)
.all();
return NextResponse.json({
moviesThisMonth: moviesThisMonth?.count ?? 0,
episodesThisWeek: episodesThisWeek?.count ?? 0,
librarySize: librarySize?.count ?? 0,
completed: completedCount?.count ?? 0,
});
}