Files
screenclipper/apps/frontend/app/segments/page.tsx
T
ayrisdevandClaude Sonnet 5 43ccbd9ec7 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>
2026-09-02 13:38:47 +03:00

134 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@streamclipper/db";
import { deleteRawSegment, deleteCandidateSegment, retryRender, cancelRender } from "../actions";
import { DeleteButton } from "../components/DeleteButton";
import { ConfirmButton } from "../components/ConfirmButton";
export const dynamic = "force-dynamic";
const STATUS_BADGE: Record<string, string> = {
PENDING: "muted",
PROCESSED: "ok",
DISCARDED: "err",
PENDING_STT: "warn",
TRANSCRIBED: "ok",
RENDERING: "warn",
READY: "ok",
FAILED: "err",
};
function transcriptPreview(transcriptJson: unknown): string | null {
if (!transcriptJson || typeof transcriptJson !== "object") return null;
const text = (transcriptJson as { text?: string }).text;
return text ? text.slice(0, 240) : null;
}
export default async function SegmentsPage() {
const segments = await prisma.rawSegment.findMany({
orderBy: { createdAt: "desc" },
take: 50,
include: { candidates: { orderBy: { createdAt: "desc" }, include: { short: true } } },
});
return (
<>
<h1>Segment & Aday Klip Kütüphanesi</h1>
{segments.length === 0 ? (
<p className="empty">Henüz işlenmiş segment yok.</p>
) : (
segments.map((segment) => (
<div className="card" key={segment.id}>
<div className="card-head">
<span className="mono">{segment.filePath}</span>
<span className={`badge ${STATUS_BADGE[segment.status] ?? "muted"}`}>{segment.status}</span>
</div>
<div className="mono">
{segment.duration}sn · {new Date(segment.startedAt).toLocaleString("tr-TR")}
</div>
<video controls preload="metadata" style={{ width: "100%", maxWidth: "480px", marginTop: "0.5rem", borderRadius: "6px" }}>
<source src={`/api/segments/${segment.id}/video`} type="video/mp4" />
</video>
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.5rem" }}>
<a className="badge muted" href={`/api/segments/${segment.id}/video?download=1`}>İndir</a>
<form action={deleteRawSegment.bind(null, segment.id)}>
<DeleteButton confirmText="Bu segmenti ve içindeki tüm aday klipleri silmek istediğine emin misin? Geri alınamaz." />
</form>
</div>
{segment.candidates.length === 0 ? (
<p className="empty" style={{ marginTop: "0.5rem" }}>
Bu segmentte aday klip bulunamadı.
</p>
) : (
segment.candidates.map((c) => (
<div key={c.id} style={{ marginTop: "0.75rem", paddingLeft: "0.75rem", borderLeft: "2px solid var(--border)" }}>
<div className="card-head">
<span className="mono">
{c.startSec}s {c.endSec}s
{c.audioPeakScore != null && ` · peak ${c.audioPeakScore.toFixed(1)}dB`}
{c.chatVelocityScore != null && " · chat spike"}
</span>
<span className={`badge ${STATUS_BADGE[c.status] ?? "muted"}`}>{c.status}</span>
</div>
{transcriptPreview(c.transcriptJson) && (
<p className="transcript-preview">{transcriptPreview(c.transcriptJson)}</p>
)}
{c.short && (
<div style={{ marginTop: "0.5rem" }}>
<span className={`badge ${STATUS_BADGE[c.short.status] ?? "muted"}`}>
9:16: {c.short.status}
</span>
{c.short.status === "READY" && (
<>
<video controls preload="metadata" style={{ width: "220px", marginTop: "0.4rem", borderRadius: "6px", display: "block" }}>
<source src={`/api/shorts/${c.short.id}/video`} type="video/mp4" />
</video>
<a className="badge muted" style={{ marginTop: "0.3rem", display: "inline-block" }} href={`/api/shorts/${c.short.id}/video?download=1`}>
İndir
</a>
</>
)}
{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?" />
</form>
</div>
))
)}
</div>
))
)}
</>
);
}