feat: interaktif panel kontrolleri (Faz 3)

Yayın durdurma, kanal duraklat/devam (tekli+toplu), şimdi kontrol et,
manuel URL ile zorla kayıt başlatma, canlı player+log önizleme, oturum
süre sayacı, başarısız/takılı render için tekrar dene ve iptal, poll
aralığı + ham segment TTL'yi panelden canlı değiştirme, cookie bayatlık
uyarısı, Telegram bildirim aç/kapa, ve kullanım/maliyet özeti (/stats).

api-daemon'ın yeni state-değiştiren endpoint'leri (stop/force-start/
render/cancel) INTERNAL_API_TOKEN ile korunuyor — bu daemon'ın portu
Coolify'de public'e açık olduğu için korumasız bırakmak güvenlik açığı
olurdu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 13:38:47 +03:00
co-authored by Claude Sonnet 5
parent 2fc3254d64
commit 43ccbd9ec7
23 changed files with 742 additions and 75 deletions
+98 -2
View File
@@ -1,10 +1,25 @@
import { prisma } from "@streamclipper/db";
import { updateYtdlpCookies } from "../actions";
import { updateYtdlpCookies, updateSystemSettings, updateNotificationPrefs } from "../actions";
export const dynamic = "force-dynamic";
const STALE_COOKIE_HOURS = 4;
export default async function SettingsPage() {
const setting = await prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } });
const [setting, pollSetting, ttlSetting, notifyStart, notifyRender] = await Promise.all([
prisma.appSetting.findUnique({ where: { key: "ytdlp_cookies" } }),
prisma.appSetting.findUnique({ where: { key: "poll_interval_ms" } }),
prisma.appSetting.findUnique({ where: { key: "raw_segment_ttl_hours" } }),
prisma.appSetting.findUnique({ where: { key: "notify_stream_start" } }),
prisma.appSetting.findUnique({ where: { key: "notify_render_done" } }),
]);
const cookieAgeHours = setting ? (Date.now() - setting.updatedAt.getTime()) / 3_600_000 : null;
const cookieStale = cookieAgeHours !== null && cookieAgeHours > STALE_COOKIE_HOURS;
const pollIntervalSec = pollSetting ? Math.round(Number(pollSetting.value) / 1000) : 60;
const ttlHours = ttlSetting ? Number(ttlSetting.value) : 24;
const notifyStreamStart = notifyStart?.value !== "false";
const notifyRenderDone = notifyRender?.value !== "false";
return (
<>
@@ -17,6 +32,14 @@ export default async function SettingsPage() {
{setting ? `son güncelleme: ${setting.updatedAt.toLocaleString("tr-TR")}` : "hiç ayarlanmadı"}
</span>
</div>
{cookieStale && (
<p style={{ marginBottom: "0.5rem" }}>
<span className="badge warn">
{Math.round(cookieAgeHours!)} saattir güncellenmedi büyük/popüler kanallarda bot-check
hatası görülebilir, taze bir cookies.txt ile güncelle
</span>
</p>
)}
<p className="empty" style={{ marginBottom: "0.5rem" }}>
YouTube canlı yayın kontrolü ve kayıt için kullanılıyor. Google, oturum çerezlerinin bir
kısmını birkaç saatte bir yeniliyor burada eskidiğini fark edersen (kanal detay
@@ -59,6 +82,79 @@ export default async function SettingsPage() {
</button>
</form>
</div>
<div className="card">
<div className="card-head">
<span>Sistem Parametreleri</span>
</div>
<p className="empty" style={{ marginBottom: "0.5rem" }}>
Bir sonraki döngüden itibaren geçerli olur, redeploy gerekmez.
</p>
<form action={updateSystemSettings} style={{ display: "flex", flexDirection: "column", gap: "0.6rem" }}>
<label className="mono" style={{ fontSize: "0.8rem" }}>
Poll aralığı (saniye)
<input name="pollIntervalSec" type="number" min={5} defaultValue={pollIntervalSec} style={{ display: "block", marginTop: "0.25rem" }} />
</label>
<label className="mono" style={{ fontSize: "0.8rem" }}>
Ham segment TTL (saat)
<input name="ttlHours" type="number" min={1} defaultValue={ttlHours} style={{ display: "block", marginTop: "0.25rem" }} />
</label>
<p className="empty" style={{ fontSize: "0.75rem", margin: 0 }}>
Eşzamanlı capture limiti ve render eşzamanlılığı BullMQ worker&apos;ları başlatılırken
sabitleniyor bunları değiştirmek için Coolify&apos;deki `MAX_CONCURRENT_CAPTURES` /
`VIDEO_RENDER_CONCURRENCY` env var&apos;larını güncelleyip redeploy etmek gerekiyor.
</p>
<button
type="submit"
style={{
alignSelf: "flex-start",
background: "var(--accent)",
border: "none",
borderRadius: "6px",
padding: "0.5rem 1rem",
color: "#fff",
fontWeight: 600,
fontSize: "0.9rem",
cursor: "pointer",
}}
>
Kaydet
</button>
</form>
</div>
<div className="card">
<div className="card-head">
<span>Bildirimler</span>
</div>
<form action={updateNotificationPrefs} style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<label className="mono" style={{ fontSize: "0.85rem", display: "flex", alignItems: "center", gap: "0.4rem" }}>
<input type="checkbox" name="notifyStreamStart" defaultChecked={notifyStreamStart} />
Yayın başladığında Telegram bildirimi
</label>
<label className="mono" style={{ fontSize: "0.85rem", display: "flex", alignItems: "center", gap: "0.4rem" }}>
<input type="checkbox" name="notifyRenderDone" defaultChecked={notifyRenderDone} />
9:16 render tamamlandığında Telegram bildirimi
</label>
<button
type="submit"
style={{
alignSelf: "flex-start",
marginTop: "0.3rem",
background: "var(--accent)",
border: "none",
borderRadius: "6px",
padding: "0.5rem 1rem",
color: "#fff",
fontWeight: 600,
fontSize: "0.9rem",
cursor: "pointer",
}}
>
Kaydet
</button>
</form>
</div>
</>
);
}