Files
screenclipper/apps/frontend/app/stats/page.tsx
T
ayrisdevandClaude Sonnet 5 7d504d1938 feat: panel UI'ını Tailwind v4 + shadcn/ui ile yeniden tasarla (Faz 4)
Elle yazılmış tek-dosya CSS (sabit koyu tema, ham table/button, window.confirm
ile silme onayı) yerine tutarlı bir bileşen sistemi: Card/Table/Badge/Button/
Input/Switch/AlertDialog primitifleri, Hanken Grotesk + JetBrains Mono
tipografi çifti, "broadcast/monitör" temalı özel bir renk paleti (camgöbeği
accent, ok/warn/err/live durum rengi sözlüğü). Tüm sayfalar (dashboard, kanal
detay, segmentler, ayarlar, istatistik, kullanıcılar, login) aynı oturumda
taşındı — silme onayları artık gerçek AlertDialog, native confirm() değil.

Veri akışı/server action'lar değişmedi, tamamen görsel katman.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 14:33:24 +03:00

64 lines
2.7 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 { Card } from "@/components/ui/card";
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 });
}
function StatCard({ label, value }: { label: string; value: string }) {
return (
<Card className="gap-1 px-5 py-4">
<span className="font-mono-num text-xs tracking-wide text-muted-foreground uppercase">{label}</span>
<span className="font-mono-num text-2xl font-semibold">{value}</span>
</Card>
);
}
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 (
<div className="flex flex-col gap-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Kullanım & Maliyet</h1>
<p className="mt-1 text-sm text-muted-foreground">
Şimdiye kadarki toplam kullanım tüm zamanlar. STT maliyeti ve disk kullanımı tahmini, gerçek faturayla
küçük farklar olabilir.
</p>
</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<StatCard label="Toplam Kayıt Süresi" value={`${fmt(captureHours)} sa`} />
<StatCard label="Toplam STT Süresi" value={`${fmt(sttMinutes)} dk`} />
<StatCard label="Tahmini Whisper Maliyeti" value={`$${fmt(sttCost, 2)}`} />
<StatCard label="Tahmini Disk Kullanımı" value={`${fmt(estimatedGb)} GB`} />
<StatCard label="Üretilen 9:16 Klip" value={String(readyShorts)} />
<StatCard label="Başarısız Render" value={String(failedShorts)} />
<StatCard label="Toplam Aday Klip" value={String(totalCandidates)} />
</div>
</div>
);
}