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>
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
import { prisma } from "@streamclipper/db";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
const STT_COST_PER_MINUTE = 0.006; // OpenAI Whisper API, doğrulanmalı — bkz. openai.com/pricing
|
||
const GB_PER_HOUR_ESTIMATE = 1.3; // 2026-09-02 oturumunda ölçülen ~2.8 Mbps ortalama bitrate'ten
|
||
|
||
function fmt(n: number, digits = 1): string {
|
||
return n.toLocaleString("tr-TR", { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
||
}
|
||
|
||
export default async function StatsPage() {
|
||
const [transcribed, durationSum, readyShorts, failedShorts, totalCandidates] = await Promise.all([
|
||
prisma.candidateSegment.findMany({
|
||
where: { status: "TRANSCRIBED" },
|
||
select: { startSec: true, endSec: true },
|
||
}),
|
||
prisma.rawSegment.aggregate({ _sum: { duration: true } }),
|
||
prisma.shortVideo.count({ where: { status: "READY" } }),
|
||
prisma.shortVideo.count({ where: { status: "FAILED" } }),
|
||
prisma.candidateSegment.count(),
|
||
]);
|
||
|
||
const sttSeconds = transcribed.reduce((sum, c) => sum + Math.max(0, c.endSec - c.startSec), 0);
|
||
const sttMinutes = sttSeconds / 60;
|
||
const sttCost = sttMinutes * STT_COST_PER_MINUTE;
|
||
|
||
const captureSeconds = durationSum._sum.duration ?? 0;
|
||
const captureHours = captureSeconds / 3600;
|
||
const estimatedGb = captureHours * GB_PER_HOUR_ESTIMATE;
|
||
|
||
return (
|
||
<>
|
||
<h1>Kullanım & Maliyet</h1>
|
||
<p className="empty" style={{ marginBottom: "1rem" }}>
|
||
Şimdiye kadarki toplam kullanım — tüm zamanlar. STT maliyeti ve disk kullanımı tahmini
|
||
(bkz. maliyet analizi), gerçek faturayla küçük farklar olabilir.
|
||
</p>
|
||
|
||
<div className="card" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: "1rem" }}>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>TOPLAM KAYIT SÜRESİ</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>{fmt(captureHours)} sa</div>
|
||
</div>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>TOPLAM STT SÜRESİ</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>{fmt(sttMinutes)} dk</div>
|
||
</div>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>TAHMİNİ WHISPER MALİYETİ</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>${fmt(sttCost, 2)}</div>
|
||
</div>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>TAHMİNİ DİSK KULLANIMI</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>{fmt(estimatedGb)} GB</div>
|
||
</div>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>ÜRETİLEN 9:16 KLİP</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>{readyShorts}</div>
|
||
</div>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>BAŞARISIZ RENDER</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>{failedShorts}</div>
|
||
</div>
|
||
<div>
|
||
<div className="mono" style={{ fontSize: "0.75rem", opacity: 0.7 }}>TOPLAM ADAY KLİP</div>
|
||
<div style={{ fontSize: "1.4rem", fontWeight: 600 }}>{totalCandidates}</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|