Ampelos dashboard: the web face, and the owner of the schema

Split out of the single Ampelos repository. Next.js app, Drizzle schema and
migrations, brand art, planning notes.

What left: scripts/, which was the agent's job library misfiled under web/ and
imported nothing from src/; and deploy/truenas, whose broadcast posts to the
scan listener on :3427 -- an agent script -- so it belongs beside the thing it
talks to.

This repository keeps the schema. The agent speaks raw SQL against the same
tables and holds no copy of it, so a rename here can break it silently where it
used to be one commit. The README says so, and the agent carries a snapshot to
check against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ryan
2026-08-15 12:12:08 +02:00
commit acdc25c797
138 changed files with 70946 additions and 0 deletions
@@ -0,0 +1,54 @@
import { auth } from "@/auth";
import { db } from "@/db/client";
import { classics, watchingNowItems } from "@/db/schema";
import { and, inArray, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export const dynamic = "force-dynamic";
type RequestBody = {
action?: "add" | "remove";
mediaItemIds?: string[];
};
async function removeActiveWatchNow(mediaItemIds: string[]) {
if (!mediaItemIds.length) return;
await db
.update(watchingNowItems)
.set({ removedAt: new Date() })
.where(and(inArray(watchingNowItems.mediaItemId, mediaItemIds), isNull(watchingNowItems.removedAt)));
}
export async function POST(request: Request) {
const session = await auth();
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
if (!session.user.isAdmin) return Response.json({ error: "Forbidden" }, { status: 403 });
const body = (await request.json()) as RequestBody;
const mediaItemIds = Array.isArray(body.mediaItemIds)
? body.mediaItemIds.filter((id): id is string => typeof id === "string" && id.length > 0)
: [];
if (!body.action || !mediaItemIds.length) {
return Response.json({ error: "Missing action or mediaItemIds" }, { status: 400 });
}
if (body.action === "add") {
await Promise.all(
mediaItemIds.map((mediaItemId) =>
db
.insert(classics)
.values({ mediaItemId, note: "Bulk marked as Timeless Classic", addedBy: session.user.id })
.onConflictDoNothing({ target: classics.mediaItemId }),
),
);
await removeActiveWatchNow(mediaItemIds);
} else {
await db.delete(classics).where(inArray(classics.mediaItemId, mediaItemIds));
}
revalidatePath("/admin/inventory");
revalidatePath("/");
return Response.json({ ok: true });
}