Repository Public
1import { Notice, PageHead, Panel, when } from "@/components/studio/ui";2import { can, requireRole } from "@/lib/auth";3import { getPolicy } from "@/lib/budget";4import { sql } from "@/lib/db";5import { env } from "@/lib/env";6import { addUser, killSwitch, removeUser, savePolicy } from "../../actions";78export const metadata = { title: "Settings" };9function N({ name, label, value, step = "0.01", hint, disabled }: { name: string; label: string; value: number; step?: string; hint?: string; disabled: boolean }) {10 return <div><label htmlFor={name} className="label">{label}</label><input id={name} name={name} type="number" step={step} min={0} defaultValue={Number(value)} className="field mt-1" disabled={disabled} />{hint && <p className="mt-1 text-xs text-[var(--color-faint)]">{hint}</p>}</div>;11}12export default async function Settings({ searchParams }: { searchParams: Promise<{ ok?: string; err?: string }> }) {13 const sp = await searchParams;14 const user = await requireRole("viewer");15 const owner = can(user, "owner");16 const [p, users, fx] = await Promise.all([17 getPolicy(),18 sql<{ id: string; email: string; name: string; role: string; totp_enabled: boolean; last_login_at: Date | null; must_change_password: boolean }[]>`SELECT id, email, name, role, totp_enabled, last_login_at, must_change_password FROM users ORDER BY created_at`,19 sql<{ value: { rate: number; at: string } }[]>`SELECT value FROM settings WHERE key = 'fx_usd_cad'`,20 ]);21 return (22 <>23 <PageHead title="Settings" sub="Budgets, autonomy and access. Only an owner can change the policy." />24 <Notice ok={sp.ok} err={sp.err} />25 <div className="grid gap-5 lg:grid-cols-[1.5fr_1fr]">26 <form action={savePolicy} className="space-y-5">27 <Panel title="Spending ceilings (USD)">28 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"><N disabled={!owner} name="per_shot_usd" label="Per shot" value={p.per_shot_usd} hint="all attempts" /><N disabled={!owner} name="per_film_usd" label="Per film" value={p.per_film_usd} /><N disabled={!owner} name="per_day_usd" label="Per day" value={p.per_day_usd} /><N disabled={!owner} name="per_month_usd" label="Per month" value={p.per_month_usd} /></div>29 <div className="mt-4 grid gap-4 sm:grid-cols-3"><N disabled={!owner} name="max_regenerations" label="Regenerations per shot" value={p.max_regenerations} step="1" hint="then owner approval" /><N disabled={!owner} name="concurrency" label="Concurrent paid jobs" value={p.concurrency} step="1" /><N disabled={!owner} name="fx" label="USD → CAD rate" value={fx[0]?.value.rate ?? 0} step="0.0001" hint={fx[0] ? `set ${when(fx[0].value.at)}` : "not set: USD only"} /></div>30 </Panel>31 <Panel title="Autonomy">32 <div className="grid gap-4 sm:grid-cols-3">33 <div><label htmlFor="default_mode" className="label">Default mode</label><select id="default_mode" name="default_mode" defaultValue={p.default_mode} className="field mt-1" disabled={!owner}><option value="manual">Manual</option><option value="assisted">Assisted</option><option value="autonomous">Autonomous with budget</option></select></div>34 <N disabled={!owner} name="films_per_day" label="Scheduler: films per day" value={p.films_per_day} step="1" /><N disabled={!owner} name="qa_threshold" label="QA threshold (0–10)" value={p.qa_threshold} step="0.1" />35 </div>36 <label className="mt-4 flex items-start gap-3 text-sm"><input type="checkbox" name="scheduler_enabled" value="1" defaultChecked={p.scheduler_enabled} disabled={!owner} className="mt-1" /><span>Enable the programming scheduler<br /><span className="text-xs text-[var(--color-faint)]">Starts films from the Programme page, inside the daily quota and every ceiling.</span></span></label>37 <label className="mt-3 flex items-start gap-3 text-sm"><input type="checkbox" name="auto_publish" value="1" defaultChecked={p.auto_publish} disabled={!owner} className="mt-1" /><span>Publish automatically when QA passes (autonomous projects only)<br /><span className="text-xs text-[var(--color-warn)]">Off by default: every release needs a human approval. A failed QA is never published automatically.</span></span></label>38 </Panel>39 {owner && <button className="btn btn-primary">Save policy</button>}40 </form>41 <div className="space-y-5">42 <Panel title="Kill switch">43 <p className="text-sm text-[var(--color-muted)]">{p.kill_switch ? "ON. No new paid job can start. Jobs already submitted to the provider cannot be cancelled through the API; they are tracked to completion and their output is stored." : "Off. Turning it on immediately stops every new paid job, the scheduler and automatic pipeline advances."}</p>44 <form action={killSwitch} className="mt-3"><input type="hidden" name="on" value={p.kill_switch ? "0" : "1"} /><input type="hidden" name="back" value="/studio/settings" /><button className={`btn ${p.kill_switch ? "btn-primary" : "btn-danger"}`} disabled={!can(user, "editor")}>{p.kill_switch ? "Release kill switch" : "Stop all new paid jobs"}</button></form>45 </Panel>46 <Panel title="Providers">47 <dl className="space-y-2 text-sm"><div className="flex justify-between gap-3"><dt className="text-[var(--color-muted)]">LLM</dt><dd className="mono">{env.llmModel}</dd></div><div className="flex justify-between gap-3"><dt className="text-[var(--color-muted)]">Image</dt><dd className="mono">{env.imageModel}</dd></div><div className="flex justify-between gap-3"><dt className="text-[var(--color-muted)]">Video (master)</dt><dd className="mono">{env.videoModel}</dd></div><div className="flex justify-between gap-3"><dt className="text-[var(--color-muted)]">Video (draft)</dt><dd className="mono">{env.videoDraftModel}</dd></div><div className="flex justify-between gap-3"><dt className="text-[var(--color-muted)]">Resolution</dt><dd className="mono">{env.videoResolution}</dd></div></dl>48 <p className="mt-3 text-xs text-[var(--color-faint)]">Set in the server environment. The API key is never shown here or sent to the browser.</p>49 </Panel>50 <Panel title="Studio members">51 <ul className="space-y-2 text-sm">{users.map((u) => <li key={u.id} className="flex items-center justify-between gap-3"><span className="min-w-0"><span className="block truncate">{u.email}</span><span className="text-xs text-[var(--color-faint)]">{u.role} · 2FA {u.totp_enabled ? "on" : "off"}{u.must_change_password ? " · one-time password not yet replaced" : ""} · last sign-in {u.last_login_at ? when(u.last_login_at) : "never"}</span></span>{owner && u.role !== "owner" && <form action={removeUser}><input type="hidden" name="user" value={u.id} /><button className="btn btn-danger btn-sm">Remove</button></form>}</li>)}</ul>52 {owner && (53 <form action={addUser} className="mt-4 space-y-3 border-t border-[var(--color-line-soft)] pt-4">54 <div><label htmlFor="email" className="label">Email</label><input id="email" name="email" type="email" required className="field mt-1" autoComplete="off" /></div>55 <div className="grid grid-cols-2 gap-3"><div><label htmlFor="name" className="label">Name</label><input id="name" name="name" className="field mt-1" autoComplete="off" /></div><div><label htmlFor="role" className="label">Role</label><select id="role" name="role" className="field mt-1"><option value="editor">Editor (produce, approve, publish)</option><option value="viewer">Viewer (read only)</option></select></div></div>56 <button className="btn btn-ghost">Add member</button>57 <p className="text-xs text-[var(--color-faint)]">A one-time password is shown once after creation; the member must replace it at first sign-in.</p>58 </form>59 )}60 </Panel>61 </div>62 </div>63 </>64 );65}66