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:
@@ -2,8 +2,19 @@ import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@streamclipper/db";
|
||||
import { fetchChannelStatuses } from "../../../lib/apiDaemon";
|
||||
import { deleteRawSegment, deleteCandidateSegment, deleteChannel } from "../../actions";
|
||||
import {
|
||||
deleteRawSegment,
|
||||
deleteCandidateSegment,
|
||||
deleteChannel,
|
||||
stopSession,
|
||||
forceStartCapture,
|
||||
retryRender,
|
||||
cancelRender,
|
||||
} from "../../actions";
|
||||
import { DeleteButton } from "../../components/DeleteButton";
|
||||
import { ConfirmButton } from "../../components/ConfirmButton";
|
||||
import { LiveLogViewer } from "../../components/LiveLogViewer";
|
||||
import { SessionTimer } from "../../components/SessionTimer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -48,6 +59,7 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
if (!channel) notFound();
|
||||
|
||||
const status = statuses.get(channel.id);
|
||||
const activeSession = channel.sessions.find((s) => s.endedAt === null);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,8 +76,21 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<span>Canlı Durum</span>
|
||||
<span className={`badge ${status?.recording ? "warn" : "muted"}`}>
|
||||
{status?.recording ? "🔴 Kayıtta" : "Kayıtta değil"}
|
||||
<span style={{ display: "flex", gap: "0.4rem", alignItems: "center" }}>
|
||||
<span className={`badge ${status?.recording ? "warn" : "muted"}`}>
|
||||
{status?.recording ? "🔴 Kayıtta" : "Kayıtta değil"}
|
||||
{status?.recording && activeSession && (
|
||||
<>
|
||||
{" · "}
|
||||
<SessionTimer startedAt={activeSession.startedAt.toISOString()} />
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{status?.recording && activeSession && (
|
||||
<form action={stopSession.bind(null, activeSession.id)}>
|
||||
<ConfirmButton confirmText="Bu kaydı şimdi durdurmak istediğine emin misin?" label="Durdur" />
|
||||
</form>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mono">
|
||||
@@ -85,6 +110,27 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{status?.recording && activeSession?.liveVideoId && activeSession.liveVideoId !== "forced" && (
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${activeSession.liveVideoId}`}
|
||||
title="Canlı yayın önizleme"
|
||||
style={{ width: "100%", maxWidth: "480px", aspectRatio: "16/9", marginTop: "0.6rem", border: "none", borderRadius: "6px" }}
|
||||
allow="autoplay; encrypted-media"
|
||||
/>
|
||||
)}
|
||||
{status?.recording && activeSession && <LiveLogViewer sessionId={activeSession.id} />}
|
||||
|
||||
<details style={{ marginTop: "0.75rem" }}>
|
||||
<summary className="mono" style={{ cursor: "pointer", fontSize: "0.8rem" }}>
|
||||
Manuel URL ile kayıt başlat
|
||||
</summary>
|
||||
<form action={forceStartCapture.bind(null, channel.id)} style={{ display: "flex", gap: "0.4rem", marginTop: "0.5rem" }}>
|
||||
<input name="url" placeholder="https://www.youtube.com/watch?v=..." required style={{ flex: 1 }} />
|
||||
<button type="submit" className="badge ok" style={{ border: "none", cursor: "pointer" }}>
|
||||
Başlat
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<h1 style={{ marginTop: "2rem" }}>Yayın Geçmişi</h1>
|
||||
@@ -153,8 +199,31 @@ export default async function ChannelDetailPage({ params }: { params: Promise<{
|
||||
{c.short.status === "FAILED" && c.short.errorMessage && (
|
||||
<p className="mono" style={{ fontSize: "0.75rem", marginTop: "0.3rem" }}>{c.short.errorMessage}</p>
|
||||
)}
|
||||
{c.short.status === "FAILED" && (
|
||||
<form action={retryRender.bind(null, c.id)} style={{ marginTop: "0.3rem" }}>
|
||||
<button type="submit" className="badge ok" style={{ border: "none", cursor: "pointer" }}>
|
||||
Tekrar Dene
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{c.short.status === "RENDERING" && (
|
||||
<form action={cancelRender.bind(null, c.id)} style={{ marginTop: "0.3rem" }}>
|
||||
<ConfirmButton
|
||||
confirmText="Render'ı başarısız say ve kilidi aç? (Çalışan process'i öldürmez, sadece durumu sıfırlar)"
|
||||
label="Takıldıysa İptal Et"
|
||||
variant="muted"
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!c.short && c.status === "TRANSCRIBED" && (
|
||||
<form action={retryRender.bind(null, c.id)} style={{ marginTop: "0.5rem" }}>
|
||||
<button type="submit" className="badge ok" style={{ border: "none", cursor: "pointer" }}>
|
||||
9:16 Render Et
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<form action={deleteCandidateSegment.bind(null, c.id)} style={{ marginTop: "0.35rem" }}>
|
||||
<DeleteButton confirmText="Bu aday klibi silmek istediğine emin misin?" />
|
||||
|
||||
Reference in New Issue
Block a user