SPB Git

Repository Public

page.tsx 5.8 KB 55 lines tsx

1import { Notice, PageHead, Panel, Status, when } from "@/components/studio/ui";2import { can, requireRole } from "@/lib/auth";3import { getPolicy } from "@/lib/budget";4import { sql } from "@/lib/db";5import { SERIES } from "@/lib/taxonomy";6import { addSchedule, toggleSchedule } from "../../actions";78export const metadata = { title: "Programme" };9const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];10export default async function Schedule({ searchParams }: { searchParams: Promise<{ ok?: string; err?: string }> }) {11  const sp = await searchParams;12  const user = await requireRole("viewer");13  const editor = can(user, "editor");14  const [policy, rows, calendar, today] = await Promise.all([15    getPolicy(),16    sql<{ id: string; title: string; theme: string; language: string; weekdays: number[]; hour: number; mode: string; enabled: boolean; last_run_on: Date | null }[]>`SELECT s.*, se.title_en AS title FROM schedules s JOIN series se ON se.id = s.series_id ORDER BY s.created_at`,17    sql<{ title_en: string; status: string; at: Date }[]>`SELECT title_en, status, COALESCE(scheduled_for, published_at) AS at FROM films WHERE status IN ('scheduled','published') AND COALESCE(scheduled_for, published_at) > now() - interval '21 days' ORDER BY at DESC LIMIT 30`,18    sql<{ n: number }[]>`SELECT COUNT(*)::int AS n FROM projects WHERE origin = 'scheduler' AND created_at >= date_trunc('day', now())`,19  ]);20  return (21    <>22      <PageHead title="Programme" sub="Recurring series and the release calendar. The scheduler only starts films: it obeys the daily quota, every budget ceiling and the kill switch, and it never publishes without the approval policy." />23      <Notice ok={sp.ok} err={sp.err} />24      <Notice err={!policy.scheduler_enabled ? "The scheduler is off (Settings → Autonomy). Programmes below are saved but will not start films." : undefined} />25      <p className="mb-4 text-sm text-[var(--color-muted)]">Quota: {today[0].n} of {policy.films_per_day} film{policy.films_per_day === 1 ? "" : "s"} started by the scheduler today (server time). Release: {policy.auto_publish ? "automatic when QA passes" : "human approval required"}.</p>26      <div className="grid gap-5 lg:grid-cols-[1.3fr_1fr]">27        <Panel title="Recurring programmes">28          {rows.length ? <ul className="space-y-3">{rows.map((r) => (29            <li key={r.id} className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[var(--color-line-soft)] p-3 text-sm">30              <div><p className="font-medium">{r.title} <span className="text-[var(--color-faint)]">· {r.language} · {r.mode}</span></p><p className="text-xs text-[var(--color-muted)]">{r.weekdays.map((d) => DAYS[d]).join(", ")} at {String(r.hour).padStart(2, "0")}:00 · {r.theme || "theme chosen by the Creative Director"}</p><p className="text-xs text-[var(--color-faint)]">Last run: {r.last_run_on ? new Date(r.last_run_on).toISOString().slice(0, 10) : "never"}</p></div>31              <div className="flex items-center gap-2"><Status value={r.enabled ? "online" : "hold"} label={r.enabled ? "enabled" : "paused"} />{editor && <><form action={toggleSchedule}><input type="hidden" name="schedule" value={r.id} /><button className="btn btn-ghost btn-sm">{r.enabled ? "Pause" : "Resume"}</button></form><form action={toggleSchedule}><input type="hidden" name="schedule" value={r.id} /><input type="hidden" name="delete" value="1" /><button className="btn btn-danger btn-sm">Delete</button></form></>}</div>32            </li>))}</ul> : <p className="text-sm text-[var(--color-faint)]">No recurring programme. Add one on the right.</p>}33          <p className="label mb-2 mt-6">Release calendar (last 3 weeks and upcoming)</p>34          {calendar.length ? <ul className="space-y-1 text-sm">{calendar.map((c, i) => <li key={i} className="flex justify-between gap-3"><span>{c.title_en}</span><span className="text-[var(--color-muted)]">{c.status === "scheduled" ? "scheduled " : ""}{when(c.at)}</span></li>)}</ul> : <p className="text-sm text-[var(--color-faint)]">Nothing released or scheduled.</p>}35        </Panel>36        {editor && (37          <Panel title="Add a programme">38            <form action={addSchedule} className="space-y-4">39              <div><label htmlFor="series" className="label">Series</label><select id="series" name="series" className="field mt-1">{SERIES.map((s) => <option key={s.slug} value={s.slug}>{s.titleEn}</option>)}</select></div>40              <div><label htmlFor="theme" className="label">Weekly theme (optional)</label><input id="theme" name="theme" maxLength={400} className="field mt-1" placeholder="e.g. Things that only exist at night" /></div>41              <fieldset><legend className="label">Weekdays</legend><div className="mt-2 flex flex-wrap gap-3 text-sm">{DAYS.map((d, i) => <label key={d} className="flex items-center gap-1.5"><input type="checkbox" name="weekdays" value={i} defaultChecked={i === 1} />{d}</label>)}</div></fieldset>42              <div className="grid grid-cols-3 gap-3">43                <div><label htmlFor="hour" className="label">Hour</label><input id="hour" name="hour" type="number" min={0} max={23} defaultValue={9} className="field mt-1" /></div>44                <div><label htmlFor="language" className="label">Language</label><select id="language" name="language" className="field mt-1"><option value="en">EN</option><option value="fr">FR</option><option value="bilingual">FR+EN</option></select></div>45                <div><label htmlFor="mode" className="label">Mode</label><select id="mode" name="mode" className="field mt-1"><option value="autonomous">Autonomous</option><option value="assisted">Assisted</option></select></div>46              </div>47              <button className="btn btn-primary">Add programme</button>48            </form>49          </Panel>50        )}51      </div>52    </>53  );54}55
Files main
Menu

Theme